Add a new ServiceSpotify, improved the structure and added new routes to modify user information and favorite music ✅
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
0ce68c7c26
commit
1a0e02bdef
After Width: | Height: | Size: 8.6 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,5 @@
|
|||||||
|
export interface IMusic {
|
||||||
|
idMusic: string;
|
||||||
|
idUser: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import { Document } from 'mongoose';
|
import { Document } from 'mongoose';
|
||||||
|
import { IMusic } from './Music';
|
||||||
|
|
||||||
export default interface User extends Document {
|
export default interface User extends Document {
|
||||||
email: string;
|
idSpotify: string;
|
||||||
|
tokenSpotify: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
idFlad: string;
|
|
||||||
idSpotify: string;
|
|
||||||
isValidPassword(password: string): Promise<Error | boolean>;
|
isValidPassword(password: string): Promise<Error | boolean>;
|
||||||
|
image: string;
|
||||||
|
musics_likes: IMusic[];
|
||||||
}
|
}
|
After Width: | Height: | Size: 1.0 KiB |
@ -1,8 +0,0 @@
|
|||||||
export default function AdjustSize(Text: string) {
|
|
||||||
const titleLength = Text.length;
|
|
||||||
const minFontSize = 23;
|
|
||||||
const maxFontSize = 48;
|
|
||||||
const fontRatio = 1.1;
|
|
||||||
const fontSize = Math.max(minFontSize, maxFontSize - (titleLength * fontRatio));
|
|
||||||
return fontSize;
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import { View } from "react-native";
|
|
||||||
import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle } from "react-native-reanimated";
|
|
||||||
|
|
||||||
const halfPi = Math.PI / 2;
|
|
||||||
|
|
||||||
export default function AnimatedParalax() {
|
|
||||||
const sensor = useAnimatedSensor(SensorType.ROTATION);
|
|
||||||
const styleAniamatedImage = useAnimatedStyle(() => {
|
|
||||||
const { pitch, roll } = sensor.sensor.value;
|
|
||||||
const verticalAxis = interpolate(
|
|
||||||
pitch,
|
|
||||||
[-halfPi, halfPi],
|
|
||||||
[-25, 25]
|
|
||||||
)
|
|
||||||
const horizontalAxis = interpolate(
|
|
||||||
roll,
|
|
||||||
[-halfPi * 2, halfPi * 2],
|
|
||||||
[-35, 35]
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
top: verticalAxis,
|
|
||||||
left: horizontalAxis,
|
|
||||||
};
|
|
||||||
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, justifyContent: 'flex-start', alignItems: 'center' }}>
|
|
||||||
<Animated.Image
|
|
||||||
source={{
|
|
||||||
uri: spot.sourceUrl,
|
|
||||||
}}
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
width: 370,
|
|
||||||
height: 370,
|
|
||||||
borderRadius: 24,
|
|
||||||
resizeMode: 'stretch',
|
|
||||||
}, styleAniamatedImage
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,71 +0,0 @@
|
|||||||
import { View, StyleSheet, Dimensions, Image, TouchableOpacity } from "react-native";
|
|
||||||
import Animated, {
|
|
||||||
Layout,
|
|
||||||
ZoomIn,
|
|
||||||
ZoomOut,
|
|
||||||
} from "react-native-reanimated";
|
|
||||||
import { Feather as Icon } from "@expo/vector-icons";
|
|
||||||
import Music from "../models/Music";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const { width } = Dimensions.get("window");
|
|
||||||
const SIZE = width / 3;
|
|
||||||
|
|
||||||
interface ArtistProps {
|
|
||||||
artist: Music;
|
|
||||||
onPress: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Artist({ artist, onPress }: ArtistProps) {
|
|
||||||
const source = typeof artist.image === 'string' ? { uri: artist.image } : artist.image;
|
|
||||||
const [selected, setSeleted] = useState(false);
|
|
||||||
const onS = () => {
|
|
||||||
setSeleted(!selected);
|
|
||||||
onPress();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<TouchableOpacity onPress={onS}>
|
|
||||||
<Animated.View
|
|
||||||
style={styles.container}
|
|
||||||
entering={ZoomIn}
|
|
||||||
exiting={ZoomOut}
|
|
||||||
layout={Layout.delay(200)}
|
|
||||||
>
|
|
||||||
<View style={styles.card}>
|
|
||||||
<Image source={source} style={styles.image} />
|
|
||||||
{selected && (
|
|
||||||
<View style={styles.cheked}>
|
|
||||||
<Icon name="check-circle" color="black" size={24} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
padding: 8,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
flex: 1,
|
|
||||||
padding: 8,
|
|
||||||
alignItems: "flex-end",
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
borderRadius: 8,
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
width: undefined,
|
|
||||||
height: undefined,
|
|
||||||
},
|
|
||||||
cheked: {
|
|
||||||
backgroundColor: "white",
|
|
||||||
borderRadius: 100,
|
|
||||||
alignItems: "center",
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,38 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { View, Text, Image, Pressable, Linking, Alert } from 'react-native'
|
|
||||||
import Artist from '../models/Artist';
|
|
||||||
|
|
||||||
interface ArtistChipProps {
|
|
||||||
backgroundColor: string;
|
|
||||||
artist: Artist;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ArtistChip({ artist }: ArtistChipProps) {
|
|
||||||
const handlePress = useCallback(async () => {
|
|
||||||
const supported = await Linking.canOpenURL(artist.url);
|
|
||||||
|
|
||||||
if (supported) {
|
|
||||||
await Linking.openURL(artist.url);
|
|
||||||
} else {
|
|
||||||
Alert.alert(`Don't know how to open this URL: ${artist.url}`);
|
|
||||||
}
|
|
||||||
}, [artist.url]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Pressable onPress={handlePress}>
|
|
||||||
<View>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: artist.image,
|
|
||||||
}}
|
|
||||||
></Image>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<Text>ii</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,34 +0,0 @@
|
|||||||
import { View, StyleSheet } from 'react-native'
|
|
||||||
import Animated, { lessThan, multiply } from 'react-native-reanimated';
|
|
||||||
import HalfCirlce from './HalfCircle';
|
|
||||||
|
|
||||||
interface CircularProps {
|
|
||||||
background: string,
|
|
||||||
foreground: string,
|
|
||||||
progress: Animated.Value<number>,
|
|
||||||
radius: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PI = Math.PI;
|
|
||||||
export default function FladInput({ background, foreground, progress }: CircularProps) {
|
|
||||||
const theta = multiply(progress, 2 * PI);
|
|
||||||
const opacity = lessThan(theta, PI);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={{ zIndex: 1 }}>
|
|
||||||
<HalfCirlce backgroundColor={background} />
|
|
||||||
<Animated.View style={{ ...StyleSheet.absoluteFillObject, transform: [{ rotate: '180%' }], opacity }}>
|
|
||||||
<HalfCirlce backgroundColor={background} />
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
|
||||||
<View style={{ transform: [{ rotate: '180%' }] }}>
|
|
||||||
<HalfCirlce backgroundColor={background} />
|
|
||||||
<Animated.View>
|
|
||||||
<HalfCirlce backgroundColor={background} />
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
import { View } from 'react-native'
|
|
||||||
|
|
||||||
interface HalfCirlceProps {
|
|
||||||
backgroundColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HalfCirlce({ backgroundColor }: HalfCirlceProps) {
|
|
||||||
return (
|
|
||||||
<View style={{
|
|
||||||
width: RADIUS * 2,
|
|
||||||
height: RADIUS * 2,
|
|
||||||
overflow: "hidden",
|
|
||||||
|
|
||||||
}}>
|
|
||||||
<View style={{ backgroundColor: backgroundColor, width: RADIUS * 2, height: RADIUS * 2, borderRadius: RADIUS, }}>
|
|
||||||
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,31 @@
|
|||||||
|
import { View, Text, StyleSheet, Image } from 'react-native';
|
||||||
|
|
||||||
|
export interface RenderCellProps {
|
||||||
|
data : any;
|
||||||
|
}
|
||||||
|
export const LittleCard = (props: RenderCellProps) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.similarContainer}>
|
||||||
|
<Image source={{ uri: props.data.cover }} style={styles.similarPoster}></Image>
|
||||||
|
<Text numberOfLines={2} style={styles.similarTitleFilm}>{props.data.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
|
||||||
|
similarContainer: {
|
||||||
|
marginHorizontal: 7
|
||||||
|
},
|
||||||
|
similarTitleFilm: {
|
||||||
|
color: "#DADADA",
|
||||||
|
paddingTop: 5,
|
||||||
|
fontWeight: "300"
|
||||||
|
},
|
||||||
|
similarPoster: {
|
||||||
|
height: 160,
|
||||||
|
width: 160,
|
||||||
|
borderRadius: 16
|
||||||
|
}
|
||||||
|
})
|
@ -1,54 +0,0 @@
|
|||||||
import Music from "../models/Music";
|
|
||||||
import { Spot } from "../models/Spot";
|
|
||||||
|
|
||||||
export const spotsData: Spot[] = [
|
|
||||||
new Spot("1", new Music("6KNw3UKRp3QRsO7Cf4ASVE",
|
|
||||||
"MOLLY - A COLORS SHOW",
|
|
||||||
"Tame Impala",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b2734299eb40408fc73ce8bf490a",
|
|
||||||
"https://p.scdn.co/mp3-preview/4faf99856f15e03a09d50b91006efd3205606866?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("2", new Music("5yHoANSze7sGzhn9MUarH3",
|
|
||||||
"Passat",
|
|
||||||
"Silk Sonic, Bruno Mars, Anderson .Paak",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b273e9df9b5a7df491536c51c922",
|
|
||||||
"https://p.scdn.co/mp3-preview/0bb7472026a00790950fc231fe61963ef7cc867b?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("3", new Music("7suNqxRED5CrwyZSzYC0nT",
|
|
||||||
"Extendo",
|
|
||||||
"Kali Uchis",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b273b856464c40a062d1723a21f2",
|
|
||||||
"https://p.scdn.co/mp3-preview/5398121f6295965e3c7cad8a6dca5667ba7f4713?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("4", new Music("07JqNLmPUJSlcouGQoJlzq",
|
|
||||||
"Addiction",
|
|
||||||
"Harry Styles",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b2739297f238f237431d56c67460",
|
|
||||||
"https://p.scdn.co/mp3-preview/33d12e9e5a3dd3394b1649d515912260b01579dd?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("5", new Music("5Ylp75kdffyJSwISRPqEiL",
|
|
||||||
"La Vidéo",
|
|
||||||
"Harry Styles",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b2738900d48677696015bf325b8b",
|
|
||||||
"https://p.scdn.co/mp3-preview/4fff3f8d76a422f42cea39f001836a3d54937fc4?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("6", new Music("30d0q6kt1BIfwAQUCAfxVQ",
|
|
||||||
"Calme toi",
|
|
||||||
"Kerchack",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b273b4f73fb5c5ea299c7ebfbf60",
|
|
||||||
"https://p.scdn.co/mp3-preview/5de1103b9528c1e47e03d32b0aa5dbfe797191a2?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("7", new Music("7IXQrRgmHxWYWitSlyFY7z",
|
|
||||||
"Peur (feat. Ziak)",
|
|
||||||
"Ziak",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b273b533a3a5625bc6ce1be56a2e",
|
|
||||||
"https://p.scdn.co/mp3-preview/2c613f31b11375980aba80a5b535bf87ddb6211b?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
),
|
|
||||||
new Spot("8", new Music("7wBoSW48q4ZFe8qSdozqqi",
|
|
||||||
"Blue",
|
|
||||||
"Kerchack",
|
|
||||||
"https://i.scdn.co/image/ab67616d0000b273cc4e66af40292c9d92146909",
|
|
||||||
"https://p.scdn.co/mp3-preview/401b51374dd3f2a15466a0b415a9ac7d2114a54b?cid=774b29d4f13844c495f206cafdad9c86")
|
|
||||||
)
|
|
||||||
|
|
||||||
];
|
|
@ -1,7 +0,0 @@
|
|||||||
Object.defineProperty(exports, '__esModule', {value: true});
|
|
||||||
require('./mqttLib');
|
|
||||||
const storage = require('./storage');
|
|
||||||
function initialize() {
|
|
||||||
global.localStorage = storage;
|
|
||||||
}
|
|
||||||
exports.default = initialize;
|
|
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
|||||||
const storage = {
|
|
||||||
setItem: (key, item) => {
|
|
||||||
storage[key] = item;
|
|
||||||
},
|
|
||||||
getItem: key => storage[key],
|
|
||||||
removeItem: key => {
|
|
||||||
delete storage[key];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default storage;
|
|
@ -1,15 +1,45 @@
|
|||||||
export default class Artist {
|
export default class Artist {
|
||||||
private id: string;
|
private _id: string;
|
||||||
private name: string;
|
private _name: string;
|
||||||
|
private _image: string;
|
||||||
private _url: string;
|
private _url: string;
|
||||||
|
|
||||||
constructor(id: string, name: string, url: string) {
|
constructor(id: string, name: string, image: string, url: string) {
|
||||||
this.id = id;
|
this._id = id;
|
||||||
this.name = name;
|
this._name = name;
|
||||||
|
this._image = image;
|
||||||
this._url = url;
|
this._url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id(): string {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
set id(value: string) {
|
||||||
|
this._id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
set name(value: string) {
|
||||||
|
this._name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get image(): string {
|
||||||
|
return this._image;
|
||||||
|
}
|
||||||
|
|
||||||
|
set image(value: string) {
|
||||||
|
this._image = value;
|
||||||
|
}
|
||||||
|
|
||||||
get url(): string {
|
get url(): string {
|
||||||
return this.url;
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
set url(value: string) {
|
||||||
|
this._url = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import EmptyMusicService from "../services/EmptyMusicService";
|
||||||
|
import IMusicService from "../services/musics/interfaces/IMusicService";
|
||||||
|
import SpotifyService from "../services/musics/spotify/SpotifyService";
|
||||||
|
|
||||||
|
export class MusicServiceProvider {
|
||||||
|
static musicService: IMusicService;
|
||||||
|
|
||||||
|
static initSpotify(refreshToken: string, idSpotify: string) {
|
||||||
|
this.musicService = new SpotifyService(refreshToken, idSpotify);
|
||||||
|
}
|
||||||
|
|
||||||
|
static resetService() {
|
||||||
|
this.musicService = new EmptyMusicService();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
export class Person {
|
||||||
|
private _id: string;
|
||||||
|
private _name: string;
|
||||||
|
public image: string;
|
||||||
|
|
||||||
|
constructor(id: string, idSpotify: string, name: string, email: string, creationDate: Date, image: string) {
|
||||||
|
this._id = id;
|
||||||
|
this._name = name;
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): string {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import Artist from "../Artist";
|
||||||
|
|
||||||
|
export default class ArtistMapper {
|
||||||
|
static toModel(artist: any): Artist {
|
||||||
|
return new Artist(artist.id, artist.name, (artist?.images?.[0]?.url ?? ""), artist.external_urls.spotify);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,18 @@
|
|||||||
import Music from "../Music";
|
import Music from "../Music";
|
||||||
|
import ArtistMapper from "./ArtistMapper";
|
||||||
|
|
||||||
export default class MusicMapper {
|
export default class MusicMapper {
|
||||||
static toModel(music: any): Music {
|
static toModel(music: any): Music {
|
||||||
|
const artists = music.artists.map((artist: any) => ArtistMapper.toModel(artist));
|
||||||
return new Music(
|
return new Music(
|
||||||
music.id,
|
music.id,
|
||||||
music.name,
|
music.name,
|
||||||
music.artists[0].name,
|
music.external_urls.spotify,
|
||||||
|
artists,
|
||||||
music.album.images[0].url,
|
music.album.images[0].url,
|
||||||
|
music.album.release_date.split('-')[0],
|
||||||
|
music.duration_ms / 1000,
|
||||||
|
music.explicit,
|
||||||
music.preview_url
|
music.preview_url
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { Spot } from "../Spot";
|
||||||
|
|
||||||
|
export class SpotMapper {
|
||||||
|
public static toModel(spot: any): Spot {
|
||||||
|
return new Spot(spot.idUser, spot.music, new Date(spot.date));
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
|
||||||
import ConversationScreen from '../screens/ConversationScreen'
|
import ConversationScreen from '../screens/ConversationScreen'
|
||||||
import ChatScreen from '../screens/ChatScreen';
|
import ChatScreen from '../screens/ChatScreen';
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
|
||||||
export default function MessagingNavigation() {
|
export default function MessagingNavigation() {
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator initialRouteName="Conversation" screenOptions={{ gestureEnabled: true, headerShown: false, cardOverlayEnabled: true, cardStyle: { backgroundColor: "transparent" } }}>
|
<Stack.Navigator initialRouteName="Conversation" >
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Conversation"
|
name="Conversation"
|
||||||
component={ConversationScreen}
|
component={ConversationScreen}
|
||||||
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Chat"
|
name="Chat"
|
||||||
component={ChatScreen}
|
component={ChatScreen}
|
||||||
options={{
|
options={{ headerShown: false }}
|
||||||
headerShown: true
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
)
|
)
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
|
||||||
import SpotScreen from '../screens/SpotScreen'
|
import SpotScreen from '../screens/SpotScreen'
|
||||||
import DetailScreen from '../screens/DetailScreen';
|
import DetailScreen from '../screens/DetailScreen';
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
|
||||||
export default function SpotNavigation() {
|
export default function SpotNavigation() {
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{
|
<Stack.Navigator>
|
||||||
gestureEnabled: false,
|
|
||||||
headerShown: false,
|
|
||||||
cardOverlayEnabled: true,
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Spot"
|
name="Spot"
|
||||||
component={SpotScreen}
|
component={SpotScreen}
|
||||||
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Detail"
|
name="Detail"
|
||||||
component={DetailScreen}
|
component={DetailScreen}
|
||||||
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import { Spot } from "../../models/Spot";
|
||||||
|
import configs from "../../constants/config";
|
||||||
|
import { MusicServiceProvider } from "../../models/MusicServiceProvider";
|
||||||
|
import { setFavoriteMusic, setUserCurrentMusic } from "../actions/appActions";
|
||||||
|
import { setAccessError, setErrorEmptyMusic } from "../actions/userActions";
|
||||||
|
import { SpotMapper } from "../../models/mapper/SpotMapper";
|
||||||
|
|
||||||
|
export const getUserCurrentMusic = () => {
|
||||||
|
//@ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
let idTrack;
|
||||||
|
const resp = await MusicServiceProvider.musicService.getCurrentlyPlayingMusic();
|
||||||
|
if (resp === null) {
|
||||||
|
idTrack = await MusicServiceProvider.musicService.getRecentlyPlayedMusic();
|
||||||
|
if (idTrack === null) {
|
||||||
|
dispatch(setErrorEmptyMusic(true));
|
||||||
|
dispatch(setUserCurrentMusic(null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idTrack = resp;
|
||||||
|
}
|
||||||
|
const music = await MusicServiceProvider.musicService.getMusicById(idTrack);
|
||||||
|
dispatch(setUserCurrentMusic(music))
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error retrieving music currently listened : " + error);
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 403:
|
||||||
|
dispatch(setAccessError(true));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dispatch(setAccessError(true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addFavoriteMusic = (spot: Spot) => {
|
||||||
|
//@ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFavoriteMusic = () => {
|
||||||
|
//@ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
let token: string | null = await SecureStore.getItemAsync(configs.key);
|
||||||
|
const headers = {
|
||||||
|
'Authorization': 'Bearer ' + token
|
||||||
|
};
|
||||||
|
const resp = await axios.get(
|
||||||
|
configs.API_URL + '/user/musics',
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
|
||||||
|
const musicIds = resp.data.musics.map((music: any) => music.idMusic);
|
||||||
|
const musics = await MusicServiceProvider.musicService.getMusicsWithIds(musicIds);
|
||||||
|
const result = resp.data.musics
|
||||||
|
.filter((music: any) => musics.some((m: any) => m.id === music.idMusic))
|
||||||
|
.map((music: any) => {
|
||||||
|
const matchingMusic = musics.find((m: any) => m.id === music.idMusic);
|
||||||
|
return {
|
||||||
|
...music,
|
||||||
|
music: matchingMusic,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(setFavoriteMusic(result.map((item: any) => SpotMapper.toModel(item))));
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
dispatch(setAccessError(true));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { Spot } from "../../models/Spot";
|
|
||||||
|
|
||||||
export const likeSpot = async (spot: Spot) => {
|
|
||||||
return async (dispatch) => {
|
|
||||||
axios.post("osdj").then(responce => {
|
|
||||||
if (responce.status == 200) {
|
|
||||||
dispatch(true);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.log("something goes wrong while searching : " + error);
|
|
||||||
;
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,69 +1,9 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as SecureStore from 'expo-secure-store';
|
|
||||||
import { Spot } from "../../models/Spot";
|
|
||||||
import SpotifyService from "../../services/spotify/spotify.service";
|
|
||||||
import { setSpotList, setUserCurrentMusic } from "../actions/spotActions";
|
|
||||||
const key = 'userToken';
|
const key = 'userToken';
|
||||||
|
|
||||||
export type CreateSpotReqBody = {
|
export const getSpotList = () => {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
artist: string;
|
|
||||||
linkCover: string;
|
|
||||||
user: string;
|
|
||||||
}
|
|
||||||
export const getSpotList = (spotsData : Record<string, string> , resuestHandler: SpotifyService) => {
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
try {
|
|
||||||
//@ts-ignore
|
|
||||||
if (spotsData) {
|
|
||||||
|
|
||||||
const spots = await Promise.all(
|
|
||||||
Object.entries(spotsData).map(async ([userId, value]) => {
|
|
||||||
const completeMusic = await resuestHandler.getMusicById(value);
|
|
||||||
return new Spot(userId, completeMusic);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(setSpotList(spots)); // our action is called here
|
|
||||||
} else {
|
|
||||||
console.log('Login Failed', 'Username or Password is incorrect');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error---------', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const getCurrentUserMusic = (resuestHandler: SpotifyService) => {
|
|
||||||
//@ts-ignore
|
|
||||||
return async dispatch => {
|
|
||||||
try {
|
|
||||||
//@ts-ignore
|
|
||||||
var currentTrackResponse = await resuestHandler.getUserCurrentMusic();
|
|
||||||
if (!currentTrackResponse) {
|
|
||||||
const recentlyTrackResponse = await resuestHandler.getUserRecentlyPlayedMusic();
|
|
||||||
if (!recentlyTrackResponse) {
|
|
||||||
throw new Error;
|
|
||||||
} else {
|
|
||||||
currentTrackResponse = recentlyTrackResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const completeMusic = await resuestHandler.getMusicById(currentTrackResponse);
|
|
||||||
if(!completeMusic){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(setUserCurrentMusic(completeMusic));
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log('Error---------', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const searchMusic = async (resuestHandler: SpotifyService, search: string) => {
|
|
||||||
|
|
||||||
return async (dispatch) => {
|
|
||||||
return resuestHandler.searchMusic(search).then(musics => dispatch((musics))).catch(err => console.log("something goes wrong while searching : " + err));
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,8 +1,72 @@
|
|||||||
import { setDarkMode } from "../actions/userActions";
|
import axios from "axios";
|
||||||
|
import configs from "../../constants/config";
|
||||||
|
import { setDarkMode, setErrorNetwork, setErrorUpdateMessage, userLogin } from "../actions/userActions";
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import { UserMapper } from "../../models/mapper/UserMapper";
|
||||||
|
|
||||||
export const darkMode = (value: boolean) => {
|
export const darkMode = (value: boolean) => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
dispatch(setDarkMode(value));
|
dispatch(setDarkMode(value));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setName = (name: string) => {
|
||||||
|
//@ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
let token: string | null = await SecureStore.getItemAsync(configs.key);
|
||||||
|
const headers = {
|
||||||
|
'Authorization': 'Bearer ' + token
|
||||||
|
};
|
||||||
|
await axios.put(configs.API_URL + '/user/name', { name }, { headers });
|
||||||
|
|
||||||
|
const user = await axios.get(
|
||||||
|
configs.API_URL + '/user',
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
dispatch(userLogin(UserMapper.toModel(user.data.data)));
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error : " + error.message);
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 409:
|
||||||
|
dispatch(setErrorUpdateMessage("Nom déjà utilisé."))
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dispatch(setErrorNetwork(true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setMail = (email: string) => {
|
||||||
|
//@ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
let token: string | null = await SecureStore.getItemAsync(configs.key);
|
||||||
|
const headers = {
|
||||||
|
'Authorization': 'Bearer ' + token
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.put(configs.API_URL + '/user/email', { email }, { headers });
|
||||||
|
|
||||||
|
const user = await axios.get(
|
||||||
|
configs.API_URL + '/user',
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
dispatch(userLogin(UserMapper.toModel(user.data.data)));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error : " + error.message);
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 409:
|
||||||
|
dispatch(setErrorUpdateMessage("Email déjà utilisé."))
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dispatch(setErrorNetwork(true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,3 @@
|
|||||||
export const playlistTypes = {
|
export const playlistTypes = {
|
||||||
FETCH_USER_PLAYLISTS: 'FETCH_SPOT',
|
|
||||||
SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST',
|
SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST',
|
||||||
FETCH_FLAD_PLAYLIST: 'FETCH_SPOT',
|
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
export const spotTypes = {
|
export const spotTypes = {
|
||||||
FETCH_SPOT: 'FETCH_SPOT',
|
FETCH_SPOT: 'FETCH_SPOT',
|
||||||
ADD_SPOT_MOCK: 'ADD_SPOT_MOCK',
|
ADD_SPOT_MOCK: 'ADD_SPOT_MOCK',
|
||||||
REMOVE_SPOT: 'REMOVE_SPOT',
|
REMOVE_SPOT: 'REMOVE_SPOT'
|
||||||
}
|
}
|
@ -1,3 +1,3 @@
|
|||||||
export const spotifyTypes = {
|
export const spotifyTypes = {
|
||||||
GET_USER_CURRENT_MUSIC: 'GET_USER_CURRENT_MUSIC',
|
GET_USER_CURRENT_MUSIC: 'GET_USER_CURRENT_MUSIC'
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
export const userTypes = {
|
export const userTypes = {
|
||||||
RESTORE_TOKEN: "RESTORE_TOKEN",
|
RESTORE_TOKEN: "RESTORE_TOKEN",
|
||||||
LOGIN: 'LOGIN',
|
LOGIN: 'LOGIN',
|
||||||
SIGNUP: 'SIGNUP',
|
|
||||||
USER_LOGOUT: 'USER_LOGOUT',
|
USER_LOGOUT: 'USER_LOGOUT',
|
||||||
SAVE_SPOTIFY: 'SAVE_SPOTIFY',
|
SAVE_SPOTIFY: 'SAVE_SPOTIFY',
|
||||||
UPDATE_USER: 'UPDATE_USER',
|
SET_ERROR_LOGIN: "SET_ERROR_LOGIN",
|
||||||
ERROR_LOGIN: "ERROR_LOGIN",
|
SET_ERROR_SIGNUP: "SET_ERROR_SIGNUP",
|
||||||
ERROR_SIGNUP: "ERROR_SIGNUP",
|
SET_DARK_MODE: "SET_DARK_MODE",
|
||||||
DARK_MODE: "DARK_MODE",
|
SET_ERROR_NETWORK: "SET_ERROR_NETWORK",
|
||||||
ERROR_NETWORK: "ERROR_NETWORK"
|
SET_ERROR_ACCESS: "SET_ERROR_ACCESS",
|
||||||
|
SET_ERROR_EMPTY_MUSIC: "SET_ERROR_EMPTY_MUSIC",
|
||||||
|
SET_ERROR_UPDATE: "SET_ERROR_UPDATE",
|
||||||
|
SET_ERROR_UPDATE_MESSAGE: "SET_ERROR_UPDATE_MESSAGE"
|
||||||
}
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import Music from "../models/Music";
|
||||||
|
import IMusicService from "./musics/interfaces/IMusicService";
|
||||||
|
|
||||||
|
export default class EmptyMusicService implements IMusicService {
|
||||||
|
getMusicById(id: string): Promise<Music> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getRecentlyPlayedMusic(): Promise<string | null> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getCurrentlyPlayingMusic(): Promise<string | null> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getMusicsWithIds(ids: string[]): Promise<Music[]> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getMusicsWithName(name: string): Promise<Music[]> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
addToPlaylist(idTrack: string): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getSimilarTracks(idTrack: string): Promise<Music[]> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import Music from "../../../models/Music";
|
||||||
|
|
||||||
|
export default interface IMusicService {
|
||||||
|
getMusicById(id: string): Promise<Music>;
|
||||||
|
getRecentlyPlayedMusic(): Promise<string | null>;
|
||||||
|
getCurrentlyPlayingMusic(): Promise<string | null>;
|
||||||
|
getMusicsWithIds(ids: string[]): Promise<Music[]>;
|
||||||
|
getMusicsWithName(name: string): Promise<Music[]>;
|
||||||
|
addToPlaylist(idTrack: string): void;
|
||||||
|
getSimilarTracks(idTrack: string): Promise<Music[]>;
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import Music from "../../../models/Music";
|
||||||
|
import IMusicService from "../interfaces/IMusicService";
|
||||||
|
import TokenSpotify from "./TokenSpotify";
|
||||||
|
import MusicMapper from "../../../models/mapper/MusicMapper";
|
||||||
|
|
||||||
|
export default class SpotifyService implements IMusicService {
|
||||||
|
private readonly API_URL = "https://api.spotify.com/v1";
|
||||||
|
private readonly PLAYLIST_NAME = "Flad's discovery";
|
||||||
|
private _token: TokenSpotify;
|
||||||
|
private _idSpotify: string;
|
||||||
|
|
||||||
|
constructor(refreshToken: string, idSpotify: string) {
|
||||||
|
this._token = new TokenSpotify(refreshToken);
|
||||||
|
this._idSpotify = idSpotify;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMusicById(id: string): Promise<Music> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.API_URL}/tracks/${id}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return MusicMapper.toModel(response.data)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("Error retrieving music information : " + error)
|
||||||
|
throw new Error("Error retrieving music information : " + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentlyPlayedMusic(limit: number = 1): Promise<string | null> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.get(`${this.API_URL}/me/player/recently-played?limit=${limit}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.data.items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response.data.items[0].track.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentlyPlayingMusic(): Promise<string | null> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
|
||||||
|
const response = await axios.get(`${this.API_URL}/me/player/currently-playing`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.item === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response.data.item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMusicsWithIds(ids: string[]): Promise<Music[]> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
var url = `${this.API_URL}/tracks?market=FR&ids=`;
|
||||||
|
if (ids.length == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
url += ids.join('%2C');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const tracksData = response.data.tracks.filter((musicData: any) => musicData !== null);
|
||||||
|
return tracksData.map((musicData: any) => MusicMapper.toModel(musicData));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMusicsWithName(name: string, limit: number = 20, offset: number = 0): Promise<Music[]> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.API_URL}/search?q=track%3A${name}&type=track&market=fr&limit=${limit}&offset=${offset}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data.tracks.items.map((musicData: any) => MusicMapper.toModel(musicData));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getPlaylistId(): Promise<string> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
const response = await axios.get(`${this.API_URL}/me/playlists?limit=50`, { headers });
|
||||||
|
const fladPlaylist = response.data.items.filter((playlist: any) => playlist.name === this.PLAYLIST_NAME);
|
||||||
|
|
||||||
|
if (fladPlaylist.length >= 1) {
|
||||||
|
return fladPlaylist[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this._createPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _createPlaylist(): Promise<string> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
const data = {
|
||||||
|
name: this.PLAYLIST_NAME,
|
||||||
|
description: 'Retrouvez toutes vos découvertes faites sur FladMusic 🎵',
|
||||||
|
public: true
|
||||||
|
};
|
||||||
|
var headers = {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
};
|
||||||
|
const response = await axios.post(`${this.API_URL}/users/${this._idSpotify}/playlists`, data, { headers });
|
||||||
|
|
||||||
|
return response.data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _isInPlaylist(idTrack: string, idPlaylist: string): Promise<Boolean> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
const response = await axios.get(`${this.API_URL}/playlists/${idPlaylist}/tracks?limit=100`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var nbTracks = response.data.items.filter((item: any) => item.track.id === idTrack).length;
|
||||||
|
|
||||||
|
return (nbTracks >= 1) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addToPlaylist(idTrack: string): Promise<void> {
|
||||||
|
var idPlaylist = await this._getPlaylistId();
|
||||||
|
|
||||||
|
if (await this._isInPlaylist(idTrack, idPlaylist)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
uris: [`spotify:track:${idTrack}`],
|
||||||
|
position: 0
|
||||||
|
};
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(`${this.API_URL}/playlists/${idPlaylist}/tracks`, data, { headers })
|
||||||
|
.then(response => {
|
||||||
|
console.log('Song successfully added to playlist.');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error adding song to playlist: ', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSimilarTracks(idTrack: string, limit: number = 20, offset: number = 0): Promise<Music[]> {
|
||||||
|
const access_token = await this._token.getAccessToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.API_URL}/recommendations?limit=${limit}&offset=${offset}&seed_tracks=${idTrack}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data.tracks.map((musicData: any) => MusicMapper.toModel(musicData));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import configs from "../../../constants/config";
|
||||||
|
|
||||||
|
export default class TokenSpotify {
|
||||||
|
private _accessToken: string = '';
|
||||||
|
private _refreshToken: string;
|
||||||
|
private _tokenEnd: Date;
|
||||||
|
|
||||||
|
constructor(refreshToken: string) {
|
||||||
|
this._refreshToken = refreshToken;
|
||||||
|
this._tokenEnd = new Date();
|
||||||
|
this._tokenEnd.setSeconds(this._tokenEnd.getSeconds() - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessToken(): Promise<string> {
|
||||||
|
if (this._isTokenExpired()) {
|
||||||
|
await this._getRefreshToken();
|
||||||
|
}
|
||||||
|
return this._accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getRefreshToken(): Promise<void> {
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
configs.API_URL + '/spotify/refresh?refresh_token=' + this._refreshToken
|
||||||
|
)
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseData = await response.data;
|
||||||
|
this._accessToken = responseData.access_token;
|
||||||
|
this._tokenEnd = new Date();
|
||||||
|
this._tokenEnd.setSeconds(this._tokenEnd.getSeconds() + responseData.expires_in)
|
||||||
|
} else {
|
||||||
|
console.log(`Error refreshing token: ${response.status}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isTokenExpired(): boolean {
|
||||||
|
return new Date() > this._tokenEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
interface IspotifyAuthHandler {
|
|
||||||
abstract async getUserToken(): Promise<string>;
|
|
||||||
abstract async getUserTokenFromRefreshToken(): Promise<string>;
|
|
||||||
}
|
|
@ -1,156 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import MusicMapper from "../../model/mapper/MusicMapper";
|
|
||||||
import Music from "../../model/Music";
|
|
||||||
import { FetchOptions, RequestHandler } from "./spotifyRequestHandler/utils";
|
|
||||||
|
|
||||||
export class MusicMinimal {
|
|
||||||
public id: string;
|
|
||||||
public title: string;
|
|
||||||
public image: string;
|
|
||||||
|
|
||||||
constructor(id: string, title: string, bio: string, image: string, trackPreviewUrl: string) {
|
|
||||||
this.title = title;
|
|
||||||
this.image = image;
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SpotifyService implements IspotifyService {
|
|
||||||
private readonly API_URL = API_URL;
|
|
||||||
private spotifyRequestHandler = new RequestHandler();
|
|
||||||
public token: string;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(token: string) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
public async getMusicById(idMusic: string): Promise<Music| null> {
|
|
||||||
let requestData: string = '/tracks/' + idMusic;
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
|
|
||||||
if (respMusic.status != 200) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
console.log(respMusic.data.artists[0].id);
|
|
||||||
|
|
||||||
return MusicFactory.mapFromSpotifyTrack(respMusic.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getUserCurrentMusic(): Promise<string | null> {
|
|
||||||
let requestData: string = '/me/player/currently-playing';
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
|
|
||||||
if (respMusic.status != 200) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return respMusic.data.item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getUserRecentlyPlayedMusic(): Promise<string | null> {
|
|
||||||
let requestData: string = '/me/player/recently-played';
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
|
|
||||||
if (respMusic.status != 200) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (respMusic.data.items.length <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return respMusic.data.items[0].track.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async playMusic(idMusic: string): Promise<void> {
|
|
||||||
const fetchOptions: FetchOptions = {
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
|
||||||
uris: [`spotify:track:${idMusic}`],
|
|
||||||
position_ms: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
throw new Error("not Implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
public async searchMusic(text: string): Promise<Music[]> {
|
|
||||||
const requestData: string = '/search';
|
|
||||||
const fetchOptions: FetchOptions = {
|
|
||||||
params: {
|
|
||||||
q: text,
|
|
||||||
type: 'track'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, fetchOptions, this.token);
|
|
||||||
|
|
||||||
const tracksData = respMusic?.data?.tracks?.items;
|
|
||||||
if (!tracksData || !Array.isArray(tracksData)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const tracks = tracksData.map((trackData: any) => {
|
|
||||||
return MusicFactory.mapFromSpotifyTrack(trackData)
|
|
||||||
});
|
|
||||||
return tracks;
|
|
||||||
}
|
|
||||||
// tempo version
|
|
||||||
public async getMusicMoreDetails(idMusic: string): Promise<string> {
|
|
||||||
let requestData: string = '/audio-features/' + idMusic;
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
|
|
||||||
return respMusic.data.audio_features.tempo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getRelatedArtist(idArtist: string): Promise<string> {
|
|
||||||
let requestData: string = '/artists/' + idArtist + '/related-artists';
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
|
|
||||||
return respMusic.data.audio_features.tempo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getArtistTopTracks(idArtist: string): Promise<string> {
|
|
||||||
let requestData: string = '/artists/' + idArtist + '/top-tracks';
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
|
|
||||||
return respMusic.data.audio_features.tempo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addItemToPlayList(playlistId: string, idMusic: string): Promise<void> {
|
|
||||||
let requestData: string = '/playlists/' + playlistId + '/tracks';
|
|
||||||
const fetchOptions: FetchOptions = {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
uris: [`spotify:track:${idMusic}`]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, fetchOptions, this.token);
|
|
||||||
console.log(respMusic.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSimilarTrack(musicId: string, limit: number = 1, market?: string): Promise<Music[]> {
|
|
||||||
const requestData: string = '/recommendations/' +
|
|
||||||
'?limit=' + limit +
|
|
||||||
'&market=FR' +
|
|
||||||
'&seed_tracks=' + musicId;
|
|
||||||
let respSimilarMusic;
|
|
||||||
try {
|
|
||||||
respSimilarMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, {}, this.token);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
if (!respSimilarMusic || !respSimilarMusic.data.tracks) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const similars: Music[] = await Promise.all(
|
|
||||||
respSimilarMusic.data.tracks.map(async (trackData: any) => {
|
|
||||||
if (trackData.id != undefined) {
|
|
||||||
const data = await this.getMusicById(trackData.id);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
)
|
|
||||||
return similars.filter((music: Music | undefined) => !!music) as Music[];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async getSpotifyCredentials() {
|
|
||||||
const res = await axios.get(this.API_URL)
|
|
||||||
const spotifyCredentials = res.data;
|
|
||||||
return spotifyCredentials
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
interface IspotifyService {
|
|
||||||
getMusicById(idMusic: string): Promise<any>;
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
|
||||||
import { MY_SECURE_AUTH_STATE_KEY, MY_SECURE_AUTH_STATE_KEY_REFRESH } from "../../../screens/RegisterScreen";
|
|
||||||
import * as SecureStore from 'expo-secure-store';
|
|
||||||
|
|
||||||
export type Methods = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
||||||
|
|
||||||
export interface FetchOptions {
|
|
||||||
/** The headers that will be applied to the request when sent. */
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
/** The type of HTTP method being used. */
|
|
||||||
method?: Methods;
|
|
||||||
/** Parameters used for search queries.*/
|
|
||||||
params?: Record<string, any>;
|
|
||||||
/**If present, this refers to the JSON data that will be included in the request body. */
|
|
||||||
body?: Record<string, string | boolean | number | (string | boolean | number)[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RequestHandler {
|
|
||||||
private _version: `v${number}` = 'v1';
|
|
||||||
|
|
||||||
get version(): string {
|
|
||||||
return this._version;
|
|
||||||
}
|
|
||||||
public async spotifyFetch(url: string, options: FetchOptions = {}, token: string): Promise<AxiosResponse<any, any>> {
|
|
||||||
try {
|
|
||||||
const resp = await axios({
|
|
||||||
url: `https://api.spotify.com/${this.version}${url}`,
|
|
||||||
method: options.method || 'GET',
|
|
||||||
params: options.params,
|
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
Accept: 'application/json',
|
|
||||||
...options.headers
|
|
||||||
},
|
|
||||||
data: options.body
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
catch(error : any){
|
|
||||||
const errorMessage = error.response.data?.error?.message;
|
|
||||||
if (errorMessage === "Invalid access token" || errorMessage === "The access token expired" ) {
|
|
||||||
console.log('### Warning ! ### try refresh token Request Handler ' +error);
|
|
||||||
|
|
||||||
const newToken = await this.refreshToken();
|
|
||||||
console.log('### GOOD Warning ! ### new token Request Handler ' +newToken);
|
|
||||||
// Mettez à jour le token dans le store ou le reducer ici
|
|
||||||
return this.spotifyFetch(url, options, newToken);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log('### Error ! ### while fetching Data in the SPotify Request Handler ' +error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private async refreshToken(): Promise<string> {
|
|
||||||
|
|
||||||
// Faites une demande à votre API pour obtenir un nouveau token Spotify
|
|
||||||
let refreshToken = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY_REFRESH);
|
|
||||||
console.log('refresh token : ' + refreshToken);
|
|
||||||
const response = await axios.get(`https://flad-api-production.up.railway.app/api/spotify/refresh?refresh_token=${refreshToken}`);
|
|
||||||
// Renvoie le nouveau token
|
|
||||||
const {
|
|
||||||
access_token : access_token,
|
|
||||||
refresh_token: refresh_token,
|
|
||||||
} = response.data as SpotifyAuthResponse
|
|
||||||
console.log('new access token : ' + access_token);
|
|
||||||
console.log('new refresh token : ' + refresh_token);
|
|
||||||
await SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, access_token);
|
|
||||||
return access_token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface SpotifyAuthResponse {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token: string;
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
import initialize from '../lib';
|
|
||||||
|
|
||||||
initialize();
|
|
||||||
class MqttClient {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const clientId = 'ReactNativeMqtt';
|
|
||||||
this.client = new Paho.MQTT.Client('127.0.0.1', 9001, clientId);
|
|
||||||
this.client.onMessageArrived = this.onMessageArrived;
|
|
||||||
this.callbacks = {};
|
|
||||||
this.onSuccessHandler = undefined;
|
|
||||||
this.onConnectionLostHandler = undefined;
|
|
||||||
this.isConnected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnect = (onSuccessHandler, onConnectionLostHandler) => {
|
|
||||||
this.onSuccessHandler = onSuccessHandler;
|
|
||||||
this.onConnectionLostHandler = onConnectionLostHandler;
|
|
||||||
this.client.onConnectionLost = () => {
|
|
||||||
this.isConnected = false;
|
|
||||||
onConnectionLostHandler();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.client.connect({
|
|
||||||
timeout: 10,
|
|
||||||
onSuccess: () => {
|
|
||||||
this.isConnected = true;
|
|
||||||
onSuccessHandler();
|
|
||||||
},
|
|
||||||
useSSL: false,
|
|
||||||
onFailure: this.onError,
|
|
||||||
reconnect: true,
|
|
||||||
keepAliveInterval: 20,
|
|
||||||
cleanSession: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onError = ({errorMessage}) => {
|
|
||||||
console.log(errorMessage);
|
|
||||||
this.isConnected = false;
|
|
||||||
Alert.alert('Failed', 'Failed to connect to MQTT', [
|
|
||||||
{
|
|
||||||
text: 'Cancel',
|
|
||||||
onPress: () => console.log('Cancel Pressed'),
|
|
||||||
style: 'cancel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Try Again',
|
|
||||||
onPress: () =>
|
|
||||||
this.onConnect(
|
|
||||||
this.onSuccessHandler,
|
|
||||||
this.onConnectionLostHandler,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMessageArrived = message => {
|
|
||||||
const {payloadString, topic} = message;
|
|
||||||
console.log('onMessageArrived:', payloadString);
|
|
||||||
this.callbacks[topic](payloadString);
|
|
||||||
};
|
|
||||||
|
|
||||||
onPublish = (topic, message) => {
|
|
||||||
this.client.publish(topic, message);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSubscribe = (topic, callback) => {
|
|
||||||
this.callbacks[topic] = callback;
|
|
||||||
this.client.subscribe(topic);
|
|
||||||
};
|
|
||||||
|
|
||||||
unsubscribe = topic => {
|
|
||||||
delete this.callbacks[topic];
|
|
||||||
this.client.unsubscribe(topic);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = new MqttClient();
|
|
||||||
export {client as MqttClient};
|
|
Loading…
Reference in new issue