From 68b87040ad19b935fab71520463c9cf63d974d5c Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 20 Dec 2025 23:59:40 +0100 Subject: [PATCH] feat(m3u-state): reducers and add selectors Move playlist reducers into a dedicated reducers module and re-export a composed playlistReducer built from smaller reducer arrays (epg, channel, playlist, favorites, filter). This extracts and organizes the previously monolithic reducer implementation into focused reducer files, improving modularity and maintainability. Add simple selectors in the new reducers index for commonly accessed pieces of PlaylistState (playlists, selected playlist id, epg availability, active channel, current EPG program, channels list, playlists loaded flag, current playlist id) so callers can import them from a single place. These changes make reducer logic easier to reason about, enable incremental testing and extension of individual reducer groups, and centralize basic selectors for consistent usage across the app. --- libs/m3u-state/src/lib/actions.ts | 198 ++++-------- libs/m3u-state/src/lib/effects.ts | 21 +- libs/m3u-state/src/lib/reducers.ts | 294 +----------------- .../src/lib/reducers/channel.reducers.ts | 29 ++ .../src/lib/reducers/epg.reducers.ts | 39 +++ .../src/lib/reducers/favorites.reducers.ts | 50 +++ .../src/lib/reducers/filter.reducers.ts | 18 ++ libs/m3u-state/src/lib/reducers/index.ts | 32 ++ .../src/lib/reducers/playlist.reducers.ts | 166 ++++++++++ 9 files changed, 413 insertions(+), 434 deletions(-) create mode 100644 libs/m3u-state/src/lib/reducers/channel.reducers.ts create mode 100644 libs/m3u-state/src/lib/reducers/epg.reducers.ts create mode 100644 libs/m3u-state/src/lib/reducers/favorites.reducers.ts create mode 100644 libs/m3u-state/src/lib/reducers/filter.reducers.ts create mode 100644 libs/m3u-state/src/lib/reducers/index.ts create mode 100644 libs/m3u-state/src/lib/reducers/playlist.reducers.ts diff --git a/libs/m3u-state/src/lib/actions.ts b/libs/m3u-state/src/lib/actions.ts index b2dd25083..2ebc8a8d7 100644 --- a/libs/m3u-state/src/lib/actions.ts +++ b/libs/m3u-state/src/lib/actions.ts @@ -1,140 +1,70 @@ -import { createAction, props } from '@ngrx/store'; +import { createActionGroup, emptyProps, props } from '@ngrx/store'; import { Channel, EpgProgram, Playlist, PlaylistMeta } from 'shared-interfaces'; -const STORE_KEY = '[GLOBAL STORE]'; -const PLAYLISTS_STORE_KEY = '[PLAYLISTS STORE]'; +export const PlaylistActions = createActionGroup({ + source: 'Playlists', + events: { + 'Load Playlists': emptyProps(), + 'Load Playlists Success': props<{ playlists: PlaylistMeta[] }>(), + 'Add Playlist': props<{ playlist: Playlist }>(), + 'Add Many Playlists': props<{ playlists: Playlist[] }>(), + 'Remove Playlist': props<{ playlistId: string }>(), + 'Update Playlist Meta': props<{ playlist: PlaylistMeta }>(), + 'Update Playlist': props<{ playlist: Playlist; playlistId: string }>(), + 'Update Many Playlists': props<{ playlists: Playlist[] }>(), + 'Parse Playlist': props<{ + uploadType: 'FILE' | 'URL' | 'TEXT'; + playlist: string; + title: string; + path?: string; + }>(), + 'Set Active Playlist': props<{ playlistId: string }>(), + 'Update Playlist Positions': props<{ + positionUpdates: { id: string; changes: { position: number } }[]; + }>(), + 'Remove All Playlists': emptyProps(), + 'Set Current Playlist Id': props<{ playlistId: string | undefined }>(), + 'Handle Adding Playlist By Url': props<{ + isTemporary: boolean; + playlist: Playlist; + }>(), + }, +}); -export const loadPlaylists = createAction( - `${PLAYLISTS_STORE_KEY} Load playlists from db` -); +export const ChannelActions = createActionGroup({ + source: 'Channels', + events: { + 'Set Channels': props<{ channels: Channel[] }>(), + 'Set Active Channel': props<{ channel: Channel }>(), + 'Set Active Channel Success': props<{ channel: Channel }>(), + 'Reset Active Channel': emptyProps(), + 'Set Adjacent Channel As Active': props<{ + direction: 'next' | 'previous'; + }>(), + }, +}); -export const loadPlaylistsSuccess = createAction( - `${PLAYLISTS_STORE_KEY} Successfully loaded playlists from db`, - props<{ playlists: PlaylistMeta[] }>() -); +export const EpgActions = createActionGroup({ + source: 'EPG', + events: { + 'Set Active Epg Program': props<{ program: EpgProgram }>(), + 'Set Current Epg Program': props<{ program: EpgProgram }>(), + 'Reset Active Epg Program': emptyProps(), + 'Set Epg Available Flag': props<{ value: boolean }>(), + }, +}); -export const addPlaylist = createAction( - `${PLAYLISTS_STORE_KEY} Add new playlist`, - props<{ playlist: Playlist }>() -); +export const FavoritesActions = createActionGroup({ + source: 'Favorites', + events: { + 'Update Favorites': props<{ channel: Channel }>(), + 'Set Favorites': props<{ channelIds: string[] }>(), + }, +}); -export const addManyPlaylists = createAction( - `${PLAYLISTS_STORE_KEY} Add many playlists`, - props<{ playlists: Playlist[] }>() -); - -export const removePlaylist = createAction( - `${PLAYLISTS_STORE_KEY} Remove playlist by id`, - props<{ playlistId: string }>() -); - -export const updatePlaylistMeta = createAction( - `${PLAYLISTS_STORE_KEY} update playlist meta`, - props<{ playlist: PlaylistMeta }>() -); - -export const updatePlaylist = createAction( - `${PLAYLISTS_STORE_KEY} update playlist`, - props<{ playlist: Playlist; playlistId: string }>() -); - -export const updateManyPlaylists = createAction( - `${PLAYLISTS_STORE_KEY} Update many playlists (auto-update mechanism)`, - props<{ playlists: Playlist[] }>() -); - -export const parsePlaylist = createAction( - `${PLAYLISTS_STORE_KEY} parse playlist`, - props<{ - uploadType: 'FILE' | 'URL' | 'TEXT'; - playlist: string; - title: string; - path?: string; - }>() -); - -export const setActivePlaylist = createAction( - `${PLAYLISTS_STORE_KEY} set active playlist`, - props<{ playlistId: string }>() -); - -export const updateFavorites = createAction( - `${STORE_KEY} Add/remove provided channel to the favorites`, - props<{ channel: Channel }>() -); - -export const setFavorites = createAction( - `${STORE_KEY} Set favorites`, - props<{ channelIds: string[] }>() -); - -export const setActiveChannel = createAction( - `${STORE_KEY} Set active channel`, - props<{ channel: Channel }>() -); - -export const resetActiveChannel = createAction( - `${STORE_KEY} Reset active channel` -); - -export const setActiveChannelSuccess = createAction( - `${STORE_KEY} Set active channel success`, - props<{ channel: Channel }>() -); - -export const setActiveEpgProgram = createAction( - `${STORE_KEY} Sets the given timestamp for the epg program`, - props<{ program: EpgProgram }>() -); - -export const setCurrentEpgProgram = createAction( - `${STORE_KEY} Updates the active epg program for the active channel`, - props<{ program: EpgProgram }>() -); - -export const resetActiveEpgProgram = createAction( - `${STORE_KEY} Reset active epg program` -); - -export const setEpgAvailableFlag = createAction( - `${STORE_KEY} Reset active epg program`, - props<{ value: boolean }>() -); - -export const setChannels = createAction( - `${STORE_KEY} Set channels`, - props<{ channels: Channel[] }>() -); - -export const updatePlaylistPositions = createAction( - `${STORE_KEY} Update playlist positions`, - props<{ - positionUpdates: { id: string; changes: { position: number } }[]; - }>() -); - -export const removeAllPlaylists = createAction( - `${STORE_KEY} Remove all playlists` -); - -export const setAdjacentChannelAsActive = createAction( - `${STORE_KEY} Set adjacent channel as active`, - props<{ - direction: 'next' | 'previous'; - }>() -); - -export const setSelectedFilters = createAction( - `${STORE_KEY} Set selected filters`, - props<{ selectedFilters: string[] }>() -); - -export const setCurrentPlaylistId = createAction( - `${STORE_KEY} Set current playlist id`, - props<{ playlistId: string | undefined }>() -); - -export const handleAddingPlaylistByUrl = createAction( - `${PLAYLISTS_STORE_KEY} Handle adding playlist by URL`, - props<{ isTemporary: boolean; playlist: Playlist }>() -); +export const FilterActions = createActionGroup({ + source: 'Filters', + events: { + 'Set Selected Filters': props<{ selectedFilters: string[] }>(), + }, +}); diff --git a/libs/m3u-state/src/lib/effects.ts b/libs/m3u-state/src/lib/effects.ts index 749d91b05..dadeb30e9 100644 --- a/libs/m3u-state/src/lib/effects.ts +++ b/libs/m3u-state/src/lib/effects.ts @@ -24,7 +24,12 @@ import { STORE_KEY, VideoPlayer, } from 'shared-interfaces'; -import * as PlaylistActions from './actions'; +import { + PlaylistActions, + ChannelActions, + EpgActions, + FavoritesActions, +} from './actions'; import { selectActive, selectActivePlaylistId, @@ -47,7 +52,7 @@ export class PlaylistEffects { updateFavorites$ = createEffect( () => { return this.actions$.pipe( - ofType(PlaylistActions.updateFavorites), + ofType(FavoritesActions.updateFavorites), combineLatestWith( this.store.select(selectFavorites), this.store.select(selectActivePlaylistId) @@ -70,7 +75,7 @@ export class PlaylistEffects { setFavorites$ = createEffect( () => { return this.actions$.pipe( - ofType(PlaylistActions.setFavorites), + ofType(FavoritesActions.setFavorites), combineLatestWith(this.store.select(selectActivePlaylistId)), filter( ([, playlistId]) => @@ -92,7 +97,7 @@ export class PlaylistEffects { setActiveEpgProgram$ = createEffect( () => { return this.actions$.pipe( - ofType(PlaylistActions.setActiveEpgProgram), + ofType(EpgActions.setActiveEpgProgram), combineLatestWith(this.store.select(selectActive)), map(([, activeChannel]) => { firstValueFrom(this.storage.get(STORE_KEY.Settings)).then( @@ -135,7 +140,7 @@ export class PlaylistEffects { setActiveChannel$ = createEffect(() => { return this.actions$.pipe( - ofType(PlaylistActions.setActiveChannel), + ofType(ChannelActions.setActiveChannel), // Skip the effect entirely when channel is falsy filter((action) => !!action.channel), map((action) => { @@ -186,7 +191,7 @@ export class PlaylistEffects { } ); - return PlaylistActions.setActiveChannelSuccess({ + return ChannelActions.setActiveChannelSuccess({ channel: action.channel, }); }) @@ -400,7 +405,7 @@ export class PlaylistEffects { setAdjacentChannelAsActive$ = createEffect(() => { return this.actions$.pipe( - ofType(PlaylistActions.setAdjacentChannelAsActive), + ofType(ChannelActions.setAdjacentChannelAsActive), withLatestFrom( this.store.select(selectChannels), this.store.select(selectActive) @@ -419,7 +424,7 @@ export class PlaylistEffects { adjacentChannel = activeChannel; adjacentChannel = channels[index - 1]; } - return PlaylistActions.setActiveChannelSuccess({ + return ChannelActions.setActiveChannelSuccess({ channel: adjacentChannel!, }); }) diff --git a/libs/m3u-state/src/lib/reducers.ts b/libs/m3u-state/src/lib/reducers.ts index 4ddd0eb59..bb775cb93 100644 --- a/libs/m3u-state/src/lib/reducers.ts +++ b/libs/m3u-state/src/lib/reducers.ts @@ -1,292 +1,2 @@ -import { createReducer, on } from '@ngrx/store'; -import moment from 'moment'; -import { Channel } from 'shared-interfaces'; -import * as PlaylistActions from './actions'; -import { playlistsAdapter } from './playlists.state'; -import { PlaylistState, initialState } from './state'; - -export const playlistReducer = createReducer( - initialState, - on(PlaylistActions.setActiveEpgProgram, (state, action): PlaylistState => { - const { program } = action; - const from = moment(program.start, 'YYYYMMDDHHmm ZZ').unix(); - const now = moment(Date.now()).unix(); - const epgParams = `?utc=${from}&lutc=${now}`; - return { - ...state, - active: { ...state.active, epgParams } as Channel, - }; - }), - on( - PlaylistActions.resetActiveEpgProgram, - (state): PlaylistState => ({ - ...state, - active: { ...state.active, epgParams: '' } as Channel, - }) - ), - on( - PlaylistActions.setActiveChannelSuccess, - (state, action): PlaylistState => { - const { channel } = action; - return { - ...state, - active: { ...channel, epgParams: '' } as Channel, - }; - } - ), - on(PlaylistActions.resetActiveChannel, (state): PlaylistState => { - return { - ...state, - active: undefined, - }; - }), - on( - PlaylistActions.setCurrentEpgProgram, - (state, action): PlaylistState => ({ - ...state, - currentEpgProgram: action.program, - }) - ), - on( - PlaylistActions.setEpgAvailableFlag, - (state, action): PlaylistState => ({ - ...state, - epgAvailable: action.value, - }) - ), - on(PlaylistActions.updateFavorites, (state, action): PlaylistState => { - let favorites; - const { channel } = action; - const playlistFavorites = - state.playlists.entities[state.playlists.selectedId]?.favorites; - if (playlistFavorites?.includes(channel.url)) { - favorites = [ - ...(playlistFavorites ?? []).filter( - (url: string) => url !== channel.url - ), - ]; - } else { - favorites = [...(playlistFavorites ?? []), channel.url]; - } - return { - ...state, - playlists: { - ...state.playlists, - entities: { - ...state.playlists.entities, - [state.playlists.selectedId]: { - ...state.playlists.entities[state.playlists.selectedId], - favorites, - }, - }, - }, - }; - }), - on(PlaylistActions.setFavorites, (state, action): PlaylistState => { - const { channelIds } = action; - return { - ...state, - playlists: { - ...state.playlists, - entities: { - ...state.playlists.entities, - [state.playlists.selectedId]: { - ...state.playlists.entities[state.playlists.selectedId], - favorites: channelIds, - }, - }, - }, - }; - }), - on(PlaylistActions.loadPlaylistsSuccess, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.addMany(action.playlists, { - ...state.playlists, - allPlaylistsLoaded: true, - }), - }; - }), - on(PlaylistActions.removePlaylist, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.removeOne( - action.playlistId, - state.playlists - ), - }; - }), - on(PlaylistActions.updatePlaylist, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.updateOne( - { - id: action.playlistId, - changes: { - ...action.playlist, - _id: action.playlistId, - updateDate: Date.now(), - count: action.playlist.playlist.items.length, - userAgent: action.playlist.userAgent, - favorites: - state.playlists.entities[action.playlistId] - ?.favorites ?? [], - }, - }, - state.playlists - ), - }; - }), - on(PlaylistActions.addPlaylist, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.addOne( - action.playlist, - state.playlists - ), - }; - }), - on(PlaylistActions.addManyPlaylists, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.addMany( - action.playlists, - state.playlists - ), - }; - }), - on(PlaylistActions.setActivePlaylist, (state, action): PlaylistState => { - return { - ...state, - playlists: { - ...state.playlists, - selectedId: action.playlistId, - }, - }; - }), - on( - PlaylistActions.updatePlaylistPositions, - (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.updateMany( - action.positionUpdates, - state.playlists - ), - }; - } - ), - on(PlaylistActions.updatePlaylistMeta, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.updateOne( - { - id: action.playlist._id, - changes: { - title: action.playlist.title, - autoRefresh: action.playlist.autoRefresh || false, - userAgent: action.playlist.userAgent, - ...(action.playlist.serverUrl !== null - ? { serverUrl: action.playlist.serverUrl } - : {}), - ...(action.playlist.username !== null - ? { username: action.playlist.username } - : {}), - ...(action.playlist.password !== null - ? { password: action.playlist.password } - : {}), - ...(action.playlist.macAddress !== null - ? { macAddress: action.playlist.macAddress } - : {}), - ...(action.playlist.portalUrl !== null - ? { portalUrl: action.playlist.portalUrl } - : {}), - }, - }, - state.playlists - ), - }; - }), - on(PlaylistActions.setChannels, (state, action): PlaylistState => { - return { - ...state, - channels: action.channels, - }; - }), - on(PlaylistActions.updateManyPlaylists, (state, action): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.updateMany( - action.playlists.map((updatedPlaylist) => ({ - id: updatedPlaylist._id, - changes: { - ...updatedPlaylist, - updateDate: Date.now(), - }, - })), - state.playlists - ), - }; - }), - on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => { - return { - ...state, - playlists: playlistsAdapter.removeAll(state.playlists), - }; - }), - on( - PlaylistActions.setSelectedFilters, - (state, { selectedFilters }): PlaylistState => { - return { - ...state, - playlists: { - ...state.playlists, - selectedFilters, - }, - }; - } - ), - on( - PlaylistActions.setCurrentPlaylistId, - (state, { playlistId }): PlaylistState => { - return { - ...state, - currentPlaylistId: playlistId, - }; - } - ), - on( - PlaylistActions.handleAddingPlaylistByUrl, - (state, action): PlaylistState => { - if (action.isTemporary) { - return { - ...state, - channels: action.playlist.playlist.items as Channel[], - }; - } else { - return { - ...state, - playlists: playlistsAdapter.addOne( - action.playlist, - state.playlists - ), - }; - } - } - ) -); - -export const selectPlaylists = (state: PlaylistState) => state.playlists; -export const selectPlaylistId = (state: PlaylistState) => - state.playlists?.selectedId; - -export const selectIsEpgAvailableReducer = (state: PlaylistState) => - state.epgAvailable; -export const selectActiveReducer = (state: PlaylistState) => state.active; -export const selectCurrentEpgProgramReducer = (state: PlaylistState) => - state.currentEpgProgram; -export const selectChannelsReducer = (state: PlaylistState) => state.channels; -export const selectPlaylistsLoadingFlagReducer = (state: PlaylistState) => - state.playlists?.allPlaylistsLoaded; -export const selectCurrentPlaylistIdReducer = (state: PlaylistState) => - state.currentPlaylistId; +// Re-export from the new modular reducers structure +export * from './reducers/index'; diff --git a/libs/m3u-state/src/lib/reducers/channel.reducers.ts b/libs/m3u-state/src/lib/reducers/channel.reducers.ts new file mode 100644 index 000000000..c4d1de1be --- /dev/null +++ b/libs/m3u-state/src/lib/reducers/channel.reducers.ts @@ -0,0 +1,29 @@ +import { on } from '@ngrx/store'; +import { Channel } from 'shared-interfaces'; +import { ChannelActions } from '../actions'; +import { PlaylistState } from '../state'; + +export const channelReducers = [ + on( + ChannelActions.setActiveChannelSuccess, + (state, action): PlaylistState => { + const { channel } = action; + return { + ...state, + active: { ...channel, epgParams: '' } as Channel, + }; + } + ), + on(ChannelActions.resetActiveChannel, (state): PlaylistState => { + return { + ...state, + active: undefined, + }; + }), + on(ChannelActions.setChannels, (state, action): PlaylistState => { + return { + ...state, + channels: action.channels, + }; + }), +]; diff --git a/libs/m3u-state/src/lib/reducers/epg.reducers.ts b/libs/m3u-state/src/lib/reducers/epg.reducers.ts new file mode 100644 index 000000000..e3f971eca --- /dev/null +++ b/libs/m3u-state/src/lib/reducers/epg.reducers.ts @@ -0,0 +1,39 @@ +import { on } from '@ngrx/store'; +import moment from 'moment'; +import { Channel } from 'shared-interfaces'; +import { EpgActions } from '../actions'; +import { PlaylistState } from '../state'; + +export const epgReducers = [ + on(EpgActions.setActiveEpgProgram, (state, action): PlaylistState => { + const { program } = action; + const from = moment(program.start, 'YYYYMMDDHHmm ZZ').unix(); + const now = moment(Date.now()).unix(); + const epgParams = `?utc=${from}&lutc=${now}`; + return { + ...state, + active: { ...state.active, epgParams } as Channel, + }; + }), + on( + EpgActions.resetActiveEpgProgram, + (state): PlaylistState => ({ + ...state, + active: { ...state.active, epgParams: '' } as Channel, + }) + ), + on( + EpgActions.setCurrentEpgProgram, + (state, action): PlaylistState => ({ + ...state, + currentEpgProgram: action.program, + }) + ), + on( + EpgActions.setEpgAvailableFlag, + (state, action): PlaylistState => ({ + ...state, + epgAvailable: action.value, + }) + ), +]; diff --git a/libs/m3u-state/src/lib/reducers/favorites.reducers.ts b/libs/m3u-state/src/lib/reducers/favorites.reducers.ts new file mode 100644 index 000000000..9b58df853 --- /dev/null +++ b/libs/m3u-state/src/lib/reducers/favorites.reducers.ts @@ -0,0 +1,50 @@ +import { on } from '@ngrx/store'; +import { FavoritesActions } from '../actions'; +import { PlaylistState } from '../state'; + +export const favoritesReducers = [ + on(FavoritesActions.updateFavorites, (state, action): PlaylistState => { + let favorites; + const { channel } = action; + const playlistFavorites = + state.playlists.entities[state.playlists.selectedId]?.favorites; + if (playlistFavorites?.includes(channel.url)) { + favorites = [ + ...(playlistFavorites ?? []).filter( + (url: string) => url !== channel.url + ), + ]; + } else { + favorites = [...(playlistFavorites ?? []), channel.url]; + } + return { + ...state, + playlists: { + ...state.playlists, + entities: { + ...state.playlists.entities, + [state.playlists.selectedId]: { + ...state.playlists.entities[state.playlists.selectedId], + favorites, + }, + }, + }, + }; + }), + on(FavoritesActions.setFavorites, (state, action): PlaylistState => { + const { channelIds } = action; + return { + ...state, + playlists: { + ...state.playlists, + entities: { + ...state.playlists.entities, + [state.playlists.selectedId]: { + ...state.playlists.entities[state.playlists.selectedId], + favorites: channelIds, + }, + }, + }, + }; + }), +]; diff --git a/libs/m3u-state/src/lib/reducers/filter.reducers.ts b/libs/m3u-state/src/lib/reducers/filter.reducers.ts new file mode 100644 index 000000000..a4533992c --- /dev/null +++ b/libs/m3u-state/src/lib/reducers/filter.reducers.ts @@ -0,0 +1,18 @@ +import { on } from '@ngrx/store'; +import { FilterActions } from '../actions'; +import { PlaylistState } from '../state'; + +export const filterReducers = [ + on( + FilterActions.setSelectedFilters, + (state, { selectedFilters }): PlaylistState => { + return { + ...state, + playlists: { + ...state.playlists, + selectedFilters, + }, + }; + } + ), +]; diff --git a/libs/m3u-state/src/lib/reducers/index.ts b/libs/m3u-state/src/lib/reducers/index.ts new file mode 100644 index 000000000..dbf3afd18 --- /dev/null +++ b/libs/m3u-state/src/lib/reducers/index.ts @@ -0,0 +1,32 @@ +import { createReducer } from '@ngrx/store'; +import { initialState, PlaylistState } from '../state'; +import { channelReducers } from './channel.reducers'; +import { epgReducers } from './epg.reducers'; +import { favoritesReducers } from './favorites.reducers'; +import { filterReducers } from './filter.reducers'; +import { playlistReducers } from './playlist.reducers'; + +export const playlistReducer = createReducer( + initialState, + ...epgReducers, + ...channelReducers, + ...playlistReducers, + ...favoritesReducers, + ...filterReducers +); + +// Selectors +export const selectPlaylists = (state: PlaylistState) => state.playlists; +export const selectPlaylistId = (state: PlaylistState) => + state.playlists?.selectedId; + +export const selectIsEpgAvailableReducer = (state: PlaylistState) => + state.epgAvailable; +export const selectActiveReducer = (state: PlaylistState) => state.active; +export const selectCurrentEpgProgramReducer = (state: PlaylistState) => + state.currentEpgProgram; +export const selectChannelsReducer = (state: PlaylistState) => state.channels; +export const selectPlaylistsLoadingFlagReducer = (state: PlaylistState) => + state.playlists?.allPlaylistsLoaded; +export const selectCurrentPlaylistIdReducer = (state: PlaylistState) => + state.currentPlaylistId; diff --git a/libs/m3u-state/src/lib/reducers/playlist.reducers.ts b/libs/m3u-state/src/lib/reducers/playlist.reducers.ts new file mode 100644 index 000000000..622cc037d --- /dev/null +++ b/libs/m3u-state/src/lib/reducers/playlist.reducers.ts @@ -0,0 +1,166 @@ +import { on } from '@ngrx/store'; +import { Channel } from 'shared-interfaces'; +import { PlaylistActions } from '../actions'; +import { playlistsAdapter } from '../playlists.state'; +import { PlaylistState } from '../state'; + +export const playlistReducers = [ + on(PlaylistActions.loadPlaylistsSuccess, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.addMany(action.playlists, { + ...state.playlists, + allPlaylistsLoaded: true, + }), + }; + }), + on(PlaylistActions.removePlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.removeOne( + action.playlistId, + state.playlists + ), + }; + }), + on(PlaylistActions.updatePlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateOne( + { + id: action.playlistId, + changes: { + ...action.playlist, + _id: action.playlistId, + updateDate: Date.now(), + count: action.playlist.playlist.items.length, + userAgent: action.playlist.userAgent, + favorites: + state.playlists.entities[action.playlistId] + ?.favorites ?? [], + }, + }, + state.playlists + ), + }; + }), + on(PlaylistActions.addPlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.addOne( + action.playlist, + state.playlists + ), + }; + }), + on(PlaylistActions.addManyPlaylists, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.addMany( + action.playlists, + state.playlists + ), + }; + }), + on(PlaylistActions.setActivePlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: { + ...state.playlists, + selectedId: action.playlistId, + }, + }; + }), + on( + PlaylistActions.updatePlaylistPositions, + (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateMany( + action.positionUpdates, + state.playlists + ), + }; + } + ), + on(PlaylistActions.updatePlaylistMeta, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateOne( + { + id: action.playlist._id, + changes: { + title: action.playlist.title, + autoRefresh: action.playlist.autoRefresh || false, + userAgent: action.playlist.userAgent, + ...(action.playlist.serverUrl !== null + ? { serverUrl: action.playlist.serverUrl } + : {}), + ...(action.playlist.username !== null + ? { username: action.playlist.username } + : {}), + ...(action.playlist.password !== null + ? { password: action.playlist.password } + : {}), + ...(action.playlist.macAddress !== null + ? { macAddress: action.playlist.macAddress } + : {}), + ...(action.playlist.portalUrl !== null + ? { portalUrl: action.playlist.portalUrl } + : {}), + }, + }, + state.playlists + ), + }; + }), + on(PlaylistActions.updateManyPlaylists, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateMany( + action.playlists.map((updatedPlaylist) => ({ + id: updatedPlaylist._id, + changes: { + ...updatedPlaylist, + updateDate: Date.now(), + }, + })), + state.playlists + ), + }; + }), + on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.removeAll(state.playlists), + }; + }), + on( + PlaylistActions.setCurrentPlaylistId, + (state, { playlistId }): PlaylistState => { + return { + ...state, + currentPlaylistId: playlistId, + }; + } + ), + on( + PlaylistActions.handleAddingPlaylistByUrl, + (state, action): PlaylistState => { + if (action.isTemporary) { + return { + ...state, + channels: action.playlist.playlist.items as Channel[], + }; + } else { + return { + ...state, + playlists: playlistsAdapter.addOne( + action.playlist, + state.playlists + ), + }; + } + } + ), +];