diff --git a/package.json b/package.json index 7818d464..bd5cee4c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dj-helper", "description": "A tool for constructing DJ sets", - "version": "0.1.4", + "version": "0.1.5", "author": "Sam Maister ", "license": "AGPL-3.0-or-later", "main": "./bundle/main.prod.js", diff --git a/src/common/reduxStore.ts b/src/common/reduxStore.ts index 0286b598..3099c05d 100644 --- a/src/common/reduxStore.ts +++ b/src/common/reduxStore.ts @@ -4,10 +4,13 @@ import offlineConfig from '@redux-offline/redux-offline/lib/defaults'; import ElectronStore from 'electron-store'; import { rootReducer, storeHydrated } from '../features/rootReducer'; import { AnyObject, AppStore } from './types'; +import { log } from '../main/helpers/console'; function createElectronStorage() { const store = new ElectronStore({}); + log(`loading config from ${store.path}...`); + return { getItem: (key: string) => Promise.resolve(store.get(key)), setItem: (key: string, item: string) => Promise.resolve(store.set(key, item)), diff --git a/src/features/embed/embedSlice.ts b/src/features/embed/embedSlice.ts index bef5d170..b23fe4c4 100644 --- a/src/features/embed/embedSlice.ts +++ b/src/features/embed/embedSlice.ts @@ -31,6 +31,7 @@ export const slice = createSlice({ case EmbedStatus.Loaded: case EmbedStatus.Idle: case EmbedStatus.Paused: + case EmbedStatus.PlayRequested: case EmbedStatus.Playing: return { ...state, trackId, loadContext: context, status: EmbedStatus.LoadRequested }; default: @@ -75,6 +76,22 @@ export const slice = createSlice({ return state; }, + mediaPlaybackError: (state) => { + log('media playback error'); + if ([EmbedStatus.PauseRequested, EmbedStatus.PlayRequested, EmbedStatus.Playing].includes(state.status)) { + return { ...state, status: EmbedStatus.Loaded }; + } + + return state; + }, + mediaLoadError: (state) => { + log('media load error'); + if ([EmbedStatus.LoadRequested].includes(state.status)) { + return { ...state, status: EmbedStatus.Idle }; + } + + return state; + }, autoplayEnabledToggled: (state, { payload: autoplayEnabled }: { payload: Embed['autoplayEnabled'] }) => ({ ...state, autoplayEnabled, @@ -90,6 +107,8 @@ export const { mediaPlaying, requestPause, mediaPaused, + mediaLoadError, + mediaPlaybackError, autoplayEnabledToggled, reset, } = slice.actions; @@ -281,10 +300,13 @@ export const trackIsLoaded = embed.trackId === trackId; export const embedRequestInFlight = - () => + ({ trackId }: { trackId: Track['id'] }) => ({ embed }: AppState): boolean => { log('embedRequestInFlight', embed); - return [EmbedStatus.LoadRequested, EmbedStatus.PauseRequested, EmbedStatus.PlayRequested].includes(embed.status); + return ( + [EmbedStatus.LoadRequested, EmbedStatus.PauseRequested, EmbedStatus.PlayRequested].includes(embed.status) && + embed.trackId === trackId + ); }; export const embedReducer = slice.reducer; diff --git a/src/features/tracks/BaseTrack.tsx b/src/features/tracks/BaseTrack.tsx index 35801ddd..6a1868ba 100644 --- a/src/features/tracks/BaseTrack.tsx +++ b/src/features/tracks/BaseTrack.tsx @@ -27,7 +27,7 @@ export function BaseTrack({ const dispatch = useAppDispatch(); const track = useAppSelector(selectTrackById(id)); const isPlaying = useAppSelector(trackIsPlaying({ trackId: id })); - const showSpinner = useAppSelector(embedRequestInFlight()); + const showSpinner = useAppSelector(embedRequestInFlight({ trackId: id })); if (!track) { return <> ; @@ -41,8 +41,8 @@ export function BaseTrack({ {title} {displayTrackDuration(duration)} - {additionalButtons} - + {additionalButtons} + { - const isPlaying = !!(await embed.webContents.executeJavaScript('$("#player").hasClass("playing");', true)); + let isPlaying = !!(await embed.webContents.executeJavaScript('$("#player").hasClass("playing");', true)); const canClick = (!isPlaying && toTrigger === 'play') || (isPlaying && toTrigger === 'pause'); log(`trigger ${toTrigger}`, isPlaying, canClick); if (canClick) { @@ -51,17 +59,42 @@ export function initEmbed(mainWindow: BrowserWindow, reduxStore: AppStore): void log('clicked', Date.now()); if (toTrigger === 'play') { lastClickPlayTime = Date.now(); + setTimeout(() => { + void (async () => { + isPlaying = !!(await embed.webContents.executeJavaScript('$("#player").hasClass("playing");', true)); + if (!isPlaying) { + log('second click incoming'); + log('clicking', Date.now()); + await embed.webContents.executeJavaScript('$("#big_play_button").click().length;', true); + log('clicked', Date.now()); + } + })(); + }, 300); + + clearTimeout(playTimeout); + playTimeout = setTimeout(() => { + void (async () => { + isPlaying = !!(await embed.webContents.executeJavaScript('$("#player").hasClass("playing");', true)); + if (!isPlaying) { + dispatch(mediaPlaybackError()); + // TODO: update status panel + } + })(); + }, 30000); } } }; const mediaStartedPlayingHandler = () => { + clearTimeout(playTimeout); log('playing from embed', Date.now()); dispatch(mediaPlaying()); }; const mediaPausedHandler = () => { void (async () => { + clearTimeout(playTimeout); + const playbackComplete = (await embed.webContents.executeJavaScript('window.playbackComplete', true)) as boolean; const currentTime = (await embed.webContents.executeJavaScript('$("#currenttime").text();', true)) as string; const pausedByTrackEnding = currentTime === '00:00' && playbackComplete; @@ -99,6 +132,7 @@ export function initEmbed(mainWindow: BrowserWindow, reduxStore: AppStore): void } const loadFinishedHandler = () => { void (async () => { + clearTimeout(loadTimeout); log('loaded', Date.now()); dispatch(mediaLoaded()); await embed.webContents.executeJavaScript( @@ -116,6 +150,10 @@ export function initEmbed(mainWindow: BrowserWindow, reduxStore: AppStore): void const embedSize = trackPreviewEmbedSize.toLowerCase(); const trackUrl = `https://bandcamp.com/EmbeddedPlayer/size=${embedSize}/bgcol=ffffff/linkcol=0687f5/track=${trackSource.sourceId}/transparent=true/`; void embed.webContents.loadURL(trackUrl); + loadTimeout = setTimeout(() => { + dispatch(mediaLoadError()); + // TODO: update status panel + }, 30000); setBounds(trackPreviewEmbedSize); }), );