From e2f515d3cf224db5e2cfc67b4edcf481f7cb49a3 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Tue, 26 Sep 2023 21:21:52 +0200 Subject: [PATCH 1/2] [feat] add inversify to inject storage service dependency --- package-lock.json | 12 +++++++ package.json | 2 ++ src/context/FlowContext.tsx | 37 ++++++++------------ src/context/InversifyContext.tsx | 23 ++++++++++++ src/context/SpotifyContext.tsx | 18 ++++++---- src/hooks/useInjection.ts | 17 +++++++++ src/inversify.config.ts | 14 ++++++++ src/main.tsx | 16 ++++++--- src/services/storage/interface.ts | 14 ++++++++ src/services/storage/localStorage.service.ts | 23 ++++++++++++ tsconfig.json | 6 +++- 11 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 src/context/InversifyContext.tsx create mode 100644 src/hooks/useInjection.ts create mode 100644 src/inversify.config.ts create mode 100644 src/services/storage/interface.ts create mode 100644 src/services/storage/localStorage.service.ts diff --git a/package-lock.json b/package-lock.json index 1327a59..bd74da3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.0", "dependencies": { "axios": "^1.5.0", + "inversify": "^6.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "reactflow": "^11.8.3", + "reflect-metadata": "^0.1.13", "use-debounce": "^9.0.4" }, "devDependencies": { @@ -2193,6 +2195,11 @@ "dev": true, "license": "ISC" }, + "node_modules/inversify": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.1.tgz", + "integrity": "sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ==" + }, "node_modules/is-binary-path": { "version": "2.1.0", "dev": true, @@ -2957,6 +2964,11 @@ "node": ">=8.10.0" } }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "node_modules/resolve": { "version": "1.22.6", "dev": true, diff --git a/package.json b/package.json index 6021ef6..d136d55 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ }, "dependencies": { "axios": "^1.5.0", + "inversify": "^6.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "reactflow": "^11.8.3", + "reflect-metadata": "^0.1.13", "use-debounce": "^9.0.4" }, "devDependencies": { diff --git a/src/context/FlowContext.tsx b/src/context/FlowContext.tsx index 02909fd..b5973d2 100644 --- a/src/context/FlowContext.tsx +++ b/src/context/FlowContext.tsx @@ -1,13 +1,9 @@ import React, { createContext, useEffect, useState } from 'react' import { Edge, Node } from 'reactflow' import { useDebounce } from 'use-debounce' - -const FLOW_LOCALSTORAGE_KEY = 'flow' - -type FlowStorage = { - nodes: Node[] - edges: Edge[] -} +import useInjection from '../hooks/useInjection' +import { IStorage } from '../services/storage/interface' +import { TYPES } from '../inversify.config' type FlowContextProps = { nodes: Node[] @@ -32,6 +28,7 @@ type FlowContextProviderProps = { const FlowContextProvider: React.FC = ({ children, }: FlowContextProviderProps) => { + const storage = useInjection(TYPES.StorageService) const [nodes, setNodes] = useState([]) const [edges, setEdges] = useState([]) const [isLoaded, setIsLoaded] = useState(false) @@ -41,32 +38,26 @@ const FlowContextProvider: React.FC = ({ useEffect(() => { if (isLoaded) return - const json = window.localStorage.getItem(FLOW_LOCALSTORAGE_KEY) - if (!json) { - console.log('no flow localstorage') + const data = storage.get('flow') + + if (!data) { setIsLoaded(true) return } - try { - const storage = JSON.parse(json) as FlowStorage - setEdges(storage.edges) - setNodes(storage.nodes) - console.log('flow localstorage loaded') - } catch (e) { - console.error(e) - } + setNodes(data.nodes) + setEdges(data.edges) setIsLoaded(true) - }, [isLoaded]) + }, [isLoaded, storage]) useEffect(() => { - if (!isLoaded) return + if (!isLoaded || (debounceEdges.length === 0 && debounceNodes.length === 0)) + return console.log('update nodes/edges') - const storage: FlowStorage = { nodes: debounceNodes, edges: debounceEdges } - window.localStorage.setItem(FLOW_LOCALSTORAGE_KEY, JSON.stringify(storage)) - }, [debounceNodes, debounceEdges, isLoaded]) + storage.set('flow', { nodes: debounceNodes, edges: debounceEdges }) + }, [debounceNodes, debounceEdges, isLoaded, storage]) return ( diff --git a/src/context/InversifyContext.tsx b/src/context/InversifyContext.tsx new file mode 100644 index 0000000..7085c0c --- /dev/null +++ b/src/context/InversifyContext.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { Container } from 'inversify' + +export const InversifyContext = React.createContext<{ + container: Container | null +}>({ + container: null, +}) + +type Props = { + container: Container + children: React.ReactElement +} + +const InversifyContextProvider: React.FC = ({ container, children }) => { + return ( + + {children} + + ) +} + +export default InversifyContextProvider diff --git a/src/context/SpotifyContext.tsx b/src/context/SpotifyContext.tsx index 0b6b3b4..3fc5cf7 100644 --- a/src/context/SpotifyContext.tsx +++ b/src/context/SpotifyContext.tsx @@ -1,4 +1,7 @@ import React, { createContext, useCallback, useEffect, useState } from 'react' +import useInjection from '../hooks/useInjection' +import { IStorage } from '../services/storage/interface' +import { TYPES } from '../inversify.config' type SpotifyContextProps = { token: string | undefined @@ -21,17 +24,20 @@ type SpotifyContextProviderProps = { const SpotifyContextProvider: React.FC = ({ children, }: SpotifyContextProviderProps) => { + const storage = useInjection(TYPES.StorageService) const [token, setToken] = useState() const logout = useCallback(() => { - window.localStorage.removeItem('token') + storage.del('music') setToken(undefined) - }, [setToken]) + }, [setToken, storage]) useEffect(() => { if (token) return - setToken(window.localStorage.getItem('token') || undefined) - }, [token]) + + const data = storage.get('music') + setToken(data?.token) + }, [token, storage]) useEffect(() => { if (!window.location.hash) return @@ -43,9 +49,9 @@ const SpotifyContextProvider: React.FC = ({ ?.split('=')[1] window.location.hash = '' - if (token) window.localStorage.setItem('token', token) + if (token) storage.set('music', { token }) setToken(token) - }, [setToken]) + }, [setToken, storage]) return ( diff --git a/src/hooks/useInjection.ts b/src/hooks/useInjection.ts new file mode 100644 index 0000000..0486947 --- /dev/null +++ b/src/hooks/useInjection.ts @@ -0,0 +1,17 @@ +import { interfaces } from 'inversify' +import { useContext, useMemo } from 'react' +import { InversifyContext } from '../context/InversifyContext' + +const useInjection = (identifier: interfaces.ServiceIdentifier): T => { + const { container } = useContext(InversifyContext) + + if (!container) { + throw new Error( + 'Inversify container not found. Make sure you have wrapped your app with the InversifyProvider.', + ) + } + + return useMemo(() => container.get(identifier), [container, identifier]) +} + +export default useInjection diff --git a/src/inversify.config.ts b/src/inversify.config.ts new file mode 100644 index 0000000..096080d --- /dev/null +++ b/src/inversify.config.ts @@ -0,0 +1,14 @@ +import { Container } from 'inversify' +import { IStorage } from './services/storage/interface' +import LocalStorageService from './services/storage/localStorage.service' + +import 'reflect-metadata' + +export const TYPES = { + StorageService: Symbol('StorageService'), +} + +const container = new Container() +container.bind(TYPES.StorageService).to(LocalStorageService) + +export default container diff --git a/src/main.tsx b/src/main.tsx index 6cf8ca5..ec85b3d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,16 +1,22 @@ +import 'reflect-metadata' + import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' import SpotifyContextProvider from './context/SpotifyContext.tsx' import FlowContextProvider from './context/FlowContext.tsx' +import InversifyContextProvider from './context/InversifyContext.tsx' +import container from './inversify.config.ts' ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - + + + + + + + , ) diff --git a/src/services/storage/interface.ts b/src/services/storage/interface.ts new file mode 100644 index 0000000..44f3044 --- /dev/null +++ b/src/services/storage/interface.ts @@ -0,0 +1,14 @@ +import { Edge, Node } from 'reactflow' + +export type StorageKey = 'music' | 'flow' + +export type StorageData = { + flow: { nodes: Node[]; edges: Edge[] } + music: { token: string } +} + +export interface IStorage { + get: (key: K) => StorageData[K] | undefined + set: (key: K, data: StorageData[K]) => void + del: (key: K) => void +} diff --git a/src/services/storage/localStorage.service.ts b/src/services/storage/localStorage.service.ts new file mode 100644 index 0000000..9e5c3fe --- /dev/null +++ b/src/services/storage/localStorage.service.ts @@ -0,0 +1,23 @@ +import { injectable } from 'inversify' +import { IStorage, StorageData, StorageKey } from './interface' + +@injectable() +class LocalStorageService implements IStorage { + get = (key: K) => { + const data = window.localStorage.getItem(key) + + if (!data) return undefined + + return JSON.parse(data) as StorageData[K] + } + + set = (key: K, data: StorageData[K]) => { + window.localStorage.setItem(key, JSON.stringify(data)) + } + + del = (key: K) => { + window.localStorage.removeItem(key) + } +} + +export default LocalStorageService diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..2160af0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,11 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + /* inversifyjs */ + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] From 7e41357f90feaaec011105d4f638d2ca2257c220 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Fri, 29 Sep 2023 10:29:55 +0200 Subject: [PATCH 2/2] [refacto] add OAuth context and spotify service --- src/Search.tsx | 75 ++++++--------- src/SearchList.tsx | 73 +++++++-------- src/context/FlowContext.tsx | 2 +- src/context/OAuthMusicContext.tsx | 126 ++++++++++++++++++++++++++ src/context/SpotifyContext.tsx | 63 ------------- src/inversify.config.ts | 14 +-- src/main.tsx | 6 +- src/services/music/interface.ts | 50 ++++++++++ src/services/music/spotify.service.ts | 34 +++++++ src/services/storage/interface.ts | 5 +- src/types.ts | 4 + 11 files changed, 291 insertions(+), 161 deletions(-) create mode 100644 src/context/OAuthMusicContext.tsx delete mode 100644 src/context/SpotifyContext.tsx create mode 100644 src/services/music/interface.ts create mode 100644 src/services/music/spotify.service.ts create mode 100644 src/types.ts diff --git a/src/Search.tsx b/src/Search.tsx index 9cd580f..6c16052 100644 --- a/src/Search.tsx +++ b/src/Search.tsx @@ -1,45 +1,23 @@ -import SearchList from './SearchList.tsx' import { useContext, useState } from 'react' -import axios from 'axios' -import { SpotifyContext } from './context/SpotifyContext.tsx' -import { FlowContext } from './context/FlowContext.tsx' - -const CLIENT_ID = '0350c90137454dc5a748549664e5ba75' -const REDIRECT_URI = 'http://localhost:5173' -const AUTH_ENDPOINT = 'https://accounts.spotify.com/authorize' -const RESPONSE_TYPE = 'token' +import { OAuthMusicContext } from './context/OAuthMusicContext' +import SearchList from './SearchList.tsx' +import { Track } from './services/music/interface.ts' function Search() { - const { token, logout } = useContext(SpotifyContext) - const { nodes, setNodes } = useContext(FlowContext) - - const [searchKey, setSearchKey] = useState('') - const [tracks, setTracks] = useState([]) - - const searchTracks = async (e) => { - e.preventDefault() - const { data } = await axios.get('https://api.spotify.com/v1/search', { - headers: { - Authorization: `Bearer ${token}`, - }, - params: { - q: searchKey, - type: 'track', - }, - }) - console.log(data) - setTracks(data.tracks.items) - } + const { login, logout } = useContext(OAuthMusicContext) + const { search, token } = useContext(OAuthMusicContext) + const [key, setKey] = useState('') + const [tracks, setTracks] = useState([]) return (
- {!token ? ( - - Login to Spotify - + Login + ) : ( - + setKey(e.target.value)} + className="col-span-4 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500" + /> +
- +
) } diff --git a/src/SearchList.tsx b/src/SearchList.tsx index 640653a..0f424f1 100644 --- a/src/SearchList.tsx +++ b/src/SearchList.tsx @@ -1,14 +1,14 @@ -import { FormEvent } from 'react' -import { Node } from 'reactflow' +import { useContext } from 'react' +import { FlowContext } from './context/FlowContext' +import { Track } from './services/music/interface' type Props = { - nodes: Node[] - setNodes: React.Dispatch> + tracks: Track[] } -function SearchList({ nodes, setNodes, tracks }: Props) { - function onSubmit(track, event: FormEvent) { - event.preventDefault() - console.log(track) +const SearchList: React.FC = ({ tracks }) => { + const { nodes, setNodes } = useContext(FlowContext) + + const onSubmit = (track: Track) => { setNodes( nodes.concat({ id: track.id, @@ -29,36 +29,33 @@ function SearchList({ nodes, setNodes, tracks }: Props) { return (
{tracks.map((track) => ( -
-
onSubmit(track, e)} - key={track.id} - > -
-

- - {track.name} -

-

- {track.artists[0].name} -

-
-
- -
-
+
+
+

+ + {track.name} +

+

+ {track.artists[0].name} +

+
+
+ +
))}
diff --git a/src/context/FlowContext.tsx b/src/context/FlowContext.tsx index b5973d2..2c7ee1d 100644 --- a/src/context/FlowContext.tsx +++ b/src/context/FlowContext.tsx @@ -3,7 +3,7 @@ import { Edge, Node } from 'reactflow' import { useDebounce } from 'use-debounce' import useInjection from '../hooks/useInjection' import { IStorage } from '../services/storage/interface' -import { TYPES } from '../inversify.config' +import { TYPES } from '../types' type FlowContextProps = { nodes: Node[] diff --git a/src/context/OAuthMusicContext.tsx b/src/context/OAuthMusicContext.tsx new file mode 100644 index 0000000..0a297b5 --- /dev/null +++ b/src/context/OAuthMusicContext.tsx @@ -0,0 +1,126 @@ +import React, { + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' +import useInjection from '../hooks/useInjection' +import { IStorage } from '../services/storage/interface' +import { TYPES } from '../types' +import { + AccessToken, + IOAuthMusicProvider, + Track, +} from '../services/music/interface' + +type OAuthMusicContextProps = { + token: AccessToken | undefined + login: () => void + logout: () => void + search: (key: string) => Promise +} + +const initialOAuthMusicContext: OAuthMusicContextProps = { + token: undefined, + login: () => {}, + logout: () => {}, + search: () => Promise.resolve([]), +} + +export const OAuthMusicContext = createContext( + initialOAuthMusicContext, +) + +type OAuthMusicContextProviderProps = { + children: React.ReactElement +} + +const OAuthMusicContextProvider: React.FC = ({ + children, +}) => { + const storage = useInjection(TYPES.StorageService) + const service = useInjection( + TYPES.IOAuthMusicProviderService, + ) + const { authEndpoint, clientId, redirectUri } = service + const [accessToken, setToken] = useState({ + token: undefined, + expires: 0, + }) + const [isLoaded, setIsLoaded] = useState(false) + const { token } = accessToken + + // TODO: verify expire + + const loginUri = useMemo( + () => + `${authEndpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=token`, + [authEndpoint, clientId, redirectUri], + ) + + const search = useCallback( + (key: string) => { + if (token) return service.search(token, key) + return Promise.resolve([]) + }, + [token, service], + ) + + const login = useCallback(() => { + console.log('OAuth: login') + window.location.assign(loginUri) + }, [loginUri]) + + const logout = useCallback(() => { + console.log('OAuth: logout') + setToken({ token: undefined, expires: 0 }) + storage.del('oauth') + }, [storage]) + + useEffect(() => { + if (!window.location.hash) return + + console.log(window.location.hash) + + const params = new URLSearchParams(window.location.hash.substring(1)) + const newToken = params.get('access_token') + const expires = params.get('expires_in') + window.location.hash = '' + + const expiresTimestamp = Number(expires) * 1000 + new Date().getTime() + + if (newToken && newToken !== token) + setToken({ token: newToken, expires: expiresTimestamp }) + }, [storage, token]) + + useEffect(() => { + if (isLoaded) return + + const storageToken = storage.get('oauth') + setIsLoaded(true) + + if (!storageToken?.token) return + + setToken(storageToken) + }, [isLoaded, storage]) + + useEffect(() => { + const storageToken = storage.get('oauth') + + if (!token || token === storageToken?.token) return + + console.log('token updated') + storage.set('oauth', accessToken) + }, [accessToken, service, storage, token]) + + return ( + + {children} + + ) +} + +export default OAuthMusicContextProvider diff --git a/src/context/SpotifyContext.tsx b/src/context/SpotifyContext.tsx deleted file mode 100644 index 3fc5cf7..0000000 --- a/src/context/SpotifyContext.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { createContext, useCallback, useEffect, useState } from 'react' -import useInjection from '../hooks/useInjection' -import { IStorage } from '../services/storage/interface' -import { TYPES } from '../inversify.config' - -type SpotifyContextProps = { - token: string | undefined - logout: () => void -} - -const initialSpotifyContext: SpotifyContextProps = { - token: undefined, - logout: () => {}, -} - -export const SpotifyContext = createContext( - initialSpotifyContext, -) - -type SpotifyContextProviderProps = { - children: React.ReactElement -} - -const SpotifyContextProvider: React.FC = ({ - children, -}: SpotifyContextProviderProps) => { - const storage = useInjection(TYPES.StorageService) - const [token, setToken] = useState() - - const logout = useCallback(() => { - storage.del('music') - setToken(undefined) - }, [setToken, storage]) - - useEffect(() => { - if (token) return - - const data = storage.get('music') - setToken(data?.token) - }, [token, storage]) - - useEffect(() => { - if (!window.location.hash) return - - const token = window.location.hash - .substring(1) - .split('&') - .find((e) => e.startsWith('access_token')) - ?.split('=')[1] - window.location.hash = '' - - if (token) storage.set('music', { token }) - setToken(token) - }, [setToken, storage]) - - return ( - - {children} - - ) -} - -export default SpotifyContextProvider diff --git a/src/inversify.config.ts b/src/inversify.config.ts index 096080d..f764d30 100644 --- a/src/inversify.config.ts +++ b/src/inversify.config.ts @@ -1,14 +1,16 @@ +import 'reflect-metadata' + import { Container } from 'inversify' import { IStorage } from './services/storage/interface' import LocalStorageService from './services/storage/localStorage.service' - -import 'reflect-metadata' - -export const TYPES = { - StorageService: Symbol('StorageService'), -} +import { TYPES } from './types' +import SpotifyService from './services/music/spotify.service' +import { IOAuthMusicProvider } from './services/music/interface' const container = new Container() container.bind(TYPES.StorageService).to(LocalStorageService) +container + .bind(TYPES.IOAuthMusicProviderService) + .to(SpotifyService) export default container diff --git a/src/main.tsx b/src/main.tsx index ec85b3d..182907b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,19 +4,19 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' -import SpotifyContextProvider from './context/SpotifyContext.tsx' import FlowContextProvider from './context/FlowContext.tsx' import InversifyContextProvider from './context/InversifyContext.tsx' import container from './inversify.config.ts' +import OAuthMusicContextProvider from './context/OAuthMusicContext.tsx' ReactDOM.createRoot(document.getElementById('root')!).render( - + - + , ) diff --git a/src/services/music/interface.ts b/src/services/music/interface.ts new file mode 100644 index 0000000..984510a --- /dev/null +++ b/src/services/music/interface.ts @@ -0,0 +1,50 @@ +export type AccessToken = { token: string | undefined; expires: number } + +export interface Track { + album: Album + artists: Artist[] + available_markets: string[] + disc_number: number + duration_ms: number + explicit: boolean + href: string + id: string + is_local: boolean + name: string + popularity: number + preview_url?: string + track_number: number + type: string + uri: string +} + +export interface Album { + album_type: string + artists: Artist[] + images: Image[] + name: string + release_date: string + release_date_precision: string + type: string + uri: string +} + +export interface Artist { + name: string + type: string + uri: string +} + +export interface Image { + height: number + url: string + width: number +} + +export interface IOAuthMusicProvider { + clientId: string + redirectUri: string + authEndpoint: string + + search: (token: string, key: string) => Promise +} diff --git a/src/services/music/spotify.service.ts b/src/services/music/spotify.service.ts new file mode 100644 index 0000000..f1880f9 --- /dev/null +++ b/src/services/music/spotify.service.ts @@ -0,0 +1,34 @@ +import { injectable } from 'inversify' +import { IOAuthMusicProvider, Track } from './interface' +import axios from 'axios' + +type SearchResult = { + tracks: { items: Track[] } +} + +@injectable() +class SpotifyService implements IOAuthMusicProvider { + public clientId: string = 'dd9b77b5b9d84bebbe346163d0275dfa' + public redirectUri: string = 'http://localhost:5173/' // TODO: change this url + public authEndpoint: string = 'https://accounts.spotify.com/authorize' + private apiEndpoint: string = 'https://api.spotify.com/v1/' + + search = async (token: string, key: string) => { + const { data } = await axios.get( + this.apiEndpoint + 'search', + { + headers: { + Authorization: `Bearer ${token}`, + }, + params: { + q: key, + type: 'track', + }, + }, + ) + + return data.tracks.items + } +} + +export default SpotifyService diff --git a/src/services/storage/interface.ts b/src/services/storage/interface.ts index 44f3044..883abce 100644 --- a/src/services/storage/interface.ts +++ b/src/services/storage/interface.ts @@ -1,10 +1,11 @@ import { Edge, Node } from 'reactflow' +import { AccessToken } from '../music/interface' -export type StorageKey = 'music' | 'flow' +export type StorageKey = 'oauth' | 'flow' export type StorageData = { flow: { nodes: Node[]; edges: Edge[] } - music: { token: string } + oauth: AccessToken } export interface IStorage { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..39fa3ac --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +export const TYPES = { + StorageService: Symbol('StorageService'), + IOAuthMusicProviderService: Symbol('IOAuthMusicProviderService'), +}