impl scrobble & scrobble setting

works even in background thanks zustand
This commit is contained in:
austinried 2021-08-04 17:59:43 +09:00
parent 706e57aa77
commit efc7e5c799
7 changed files with 106 additions and 13 deletions

View File

@ -4,6 +4,7 @@ export interface Server {
username: string username: string
token: string token: string
salt: string salt: string
scrobble: boolean
} }
export interface AppSettings { export interface AppSettings {

View File

@ -1,8 +1,36 @@
import { getCurrentTrack, getPlayerState, trackPlayerCommands } from '@app/state/trackplayer' import { getCurrentTrack, getPlayerState, TrackExt, trackPlayerCommands } from '@app/state/trackplayer'
import TrackPlayer, { Event } from 'react-native-track-player' import TrackPlayer, { Event, State } from 'react-native-track-player'
import { useStore } from './state/store' import { useStore } from './state/store'
import { unstable_batchedUpdates } from 'react-native'
const reset = () => {
unstable_batchedUpdates(() => {
useStore.getState().reset()
})
}
const setPlayerState = (state: State) => {
unstable_batchedUpdates(() => {
useStore.getState().setPlayerState(state)
})
}
const setCurrentTrackIdx = (idx?: number) => {
unstable_batchedUpdates(() => {
useStore.getState().setCurrentTrackIdx(idx)
})
}
module.exports = async function () { module.exports = async function () {
const unsubCurrentTrack = useStore.subscribe(
(currentTrack?: TrackExt) => {
if (currentTrack) {
useStore.getState().scrobbleTrack(currentTrack.id)
}
},
state => state.currentTrack,
)
TrackPlayer.addEventListener(Event.RemotePlay, () => trackPlayerCommands.enqueue(TrackPlayer.play)) TrackPlayer.addEventListener(Event.RemotePlay, () => trackPlayerCommands.enqueue(TrackPlayer.play))
TrackPlayer.addEventListener(Event.RemotePause, () => trackPlayerCommands.enqueue(TrackPlayer.pause)) TrackPlayer.addEventListener(Event.RemotePause, () => trackPlayerCommands.enqueue(TrackPlayer.pause))
@ -27,30 +55,31 @@ module.exports = async function () {
}) })
TrackPlayer.addEventListener(Event.RemoteStop, () => { TrackPlayer.addEventListener(Event.RemoteStop, () => {
useStore.getState().reset() unsubCurrentTrack()
reset()
trackPlayerCommands.enqueue(TrackPlayer.destroy) trackPlayerCommands.enqueue(TrackPlayer.destroy)
}) })
TrackPlayer.addEventListener(Event.PlaybackState, () => { TrackPlayer.addEventListener(Event.PlaybackState, () => {
trackPlayerCommands.enqueue(async () => { trackPlayerCommands.enqueue(async () => {
useStore.getState().setPlayerState(await getPlayerState()) setPlayerState(await getPlayerState())
}) })
}) })
TrackPlayer.addEventListener(Event.PlaybackTrackChanged, () => { TrackPlayer.addEventListener(Event.PlaybackTrackChanged, () => {
useStore.getState().setProgress({ position: 0, duration: 0, buffered: 0 }) useStore.getState().setProgress({ position: 0, duration: 0, buffered: 0 })
trackPlayerCommands.enqueue(async () => { trackPlayerCommands.enqueue(async () => {
useStore.getState().setCurrentTrackIdx(await getCurrentTrack()) setCurrentTrackIdx(await getCurrentTrack())
}) })
}) })
TrackPlayer.addEventListener(Event.PlaybackQueueEnded, () => { TrackPlayer.addEventListener(Event.PlaybackQueueEnded, () => {
trackPlayerCommands.enqueue(async () => { trackPlayerCommands.enqueue(async () => {
useStore.getState().setCurrentTrackIdx(await getCurrentTrack()) setCurrentTrackIdx(await getCurrentTrack())
}) })
}) })
TrackPlayer.addEventListener(Event.PlaybackMetadataReceived, () => { TrackPlayer.addEventListener(Event.PlaybackMetadataReceived, () => {
useStore.getState().setCurrentTrackIdx(useStore.getState().currentTrackIdx) setCurrentTrackIdx(useStore.getState().currentTrackIdx)
}) })
} }

View File

@ -32,6 +32,7 @@ const ServerView: React.FC<{
const [address, setAddress] = useState(server?.address || '') const [address, setAddress] = useState(server?.address || '')
const [username, setUsername] = useState(server?.username || '') const [username, setUsername] = useState(server?.username || '')
const [password, setPassword] = useState(server?.token ? 'password' : '') const [password, setPassword] = useState(server?.token ? 'password' : '')
const [scrobble, setScrobble] = useState(server?.scrobble || false)
const validate = useCallback(() => { const validate = useCallback(() => {
return !!address && !!username && !!password return !!address && !!username && !!password
@ -49,7 +50,7 @@ const ServerView: React.FC<{
} }
}, [navigation]) }, [navigation])
const save = () => { const save = useCallback(() => {
if (!validate()) { if (!validate()) {
return return
} }
@ -68,6 +69,7 @@ const ServerView: React.FC<{
username, username,
salt, salt,
token, token,
scrobble,
} }
if (server) { if (server) {
@ -87,7 +89,20 @@ const ServerView: React.FC<{
} }
exit() exit()
} }, [
activeServer,
address,
exit,
id,
password,
scrobble,
server,
servers,
setActiveServer,
setServers,
username,
validate,
])
const remove = useCallback(() => { const remove = useCallback(() => {
if (!canRemove()) { if (!canRemove()) {
@ -140,14 +155,17 @@ const ServerView: React.FC<{
value={password} value={password}
onChangeText={setPassword} onChangeText={setPassword}
/> />
<SettingsItem title="Scrobble" subtitle="Don't scrobble play history"> <SettingsItem
title="Scrobble plays"
subtitle={scrobble ? 'Scrobble play history' : "Don't scrobble play history"}>
<Switch <Switch
trackColor={{ trackColor={{
false: colors.accentLow, false: colors.accentLow,
true: colors.accent, true: colors.accent,
}} }}
thumbColor={colors.text.primary} thumbColor={colors.text.primary}
value={false} value={scrobble}
onValueChange={setScrobble}
/> />
</SettingsItem> </SettingsItem>
<Button <Button

View File

@ -9,6 +9,7 @@ export type SettingsSlice = {
client?: SubsonicApiClient client?: SubsonicApiClient
createClient: (id?: string) => void createClient: (id?: string) => void
setActiveServer: (id?: string) => void setActiveServer: (id?: string) => void
getActiveServer: () => Server | undefined
setServers: (servers: Server[]) => void setServers: (servers: Server[]) => void
} }
@ -25,7 +26,7 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
return return
} }
const server = get().settings.servers.find(s => s.id === id) const server = get().getActiveServer()
if (!server) { if (!server) {
set({ client: undefined }) set({ client: undefined })
return return
@ -52,10 +53,15 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
}), }),
) )
}, },
getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer),
setServers: servers => setServers: servers =>
set( set(
produce<SettingsSlice>(state => { produce<SettingsSlice>(state => {
state.settings.servers = servers state.settings.servers = servers
const activeServer = servers.find(s => s.id === state.settings.activeServer)
if (activeServer) {
state.client = new SubsonicApiClient(activeServer)
}
}), }),
), ),
}) })

View File

@ -35,6 +35,8 @@ export type TrackPlayerSlice = {
progress: Progress progress: Progress
setProgress: (progress: Progress) => void setProgress: (progress: Progress) => void
scrobbleTrack: (id: string) => Promise<void>
reset: () => void reset: () => void
} }
@ -59,12 +61,14 @@ export const selectTrackPlayer = {
progress: (store: TrackPlayerSlice) => store.progress, progress: (store: TrackPlayerSlice) => store.progress,
setProgress: (store: TrackPlayerSlice) => store.setProgress, setProgress: (store: TrackPlayerSlice) => store.setProgress,
scrobbleTrack: (store: TrackPlayerSlice) => store.scrobbleTrack,
reset: (store: TrackPlayerSlice) => store.reset, reset: (store: TrackPlayerSlice) => store.reset,
} }
export const trackPlayerCommands = new PromiseQueue(1) export const trackPlayerCommands = new PromiseQueue(1)
export const createTrackPlayerSlice = (set: SetState<Store>, _get: GetState<Store>): TrackPlayerSlice => ({ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerSlice => ({
name: undefined, name: undefined,
setName: name => set({ name }), setName: name => set({ name }),
@ -91,6 +95,21 @@ export const createTrackPlayerSlice = (set: SetState<Store>, _get: GetState<Stor
progress: { position: 0, duration: 0, buffered: 0 }, progress: { position: 0, duration: 0, buffered: 0 },
setProgress: progress => set({ progress }), setProgress: progress => set({ progress }),
scrobbleTrack: async id => {
const client = get().client
if (!client) {
return
}
if (!get().getActiveServer()?.scrobble) {
return
}
try {
await client.scrobble({ id })
} catch {}
},
reset: () => { reset: () => {
set({ set({
name: undefined, name: undefined,

View File

@ -13,6 +13,7 @@ import {
GetPlaylistParams, GetPlaylistParams,
GetPlaylistsParams, GetPlaylistsParams,
GetTopSongsParams, GetTopSongsParams,
ScrobbleParams,
Search3Params, Search3Params,
StreamParams, StreamParams,
} from '@app/subsonic/params' } from '@app/subsonic/params'
@ -223,6 +224,15 @@ export class SubsonicApiClient {
return this.buildUrl('stream', params) return this.buildUrl('stream', params)
} }
//
// Media annotation
//
async scrobble(params: ScrobbleParams): Promise<SubsonicResponse<undefined>> {
const xml = await this.apiGetXml('scrobble', params)
return new SubsonicResponse<undefined>(xml, undefined)
}
// //
// Searching // Searching
// //

View File

@ -100,6 +100,16 @@ export type StreamParams = {
estimateContentLength?: boolean estimateContentLength?: boolean
} }
//
// Media annotation
//
export type ScrobbleParams = {
id: string
time?: Date
submission?: boolean
}
// //
// Searching // Searching
// //