import ApiClient from './ApiClient'; import * as PL from './types/Playlist'; import urls from './urls'; import { mapResourceResponse, mapResourceV2Response, mapCreateTag, } from './ResourceClient'; import { mapPresentationResponse, mapPresentationV2Response, } from './PresentationClient'; import { mapApplicationV2Response, mapApplicationVersionResponse, } from './ApplicationClient'; import { mapThemeV2Response } from './ThemeClient'; import mapObjectValues from './mapObjectValues'; const mapPlaylistItemResponse = ( item: PL.PlaylistItemResponse, ): PL.PlaylistItem => ({ // Playlist item ids are auto-incrementing numbers but should probably be UUIDs in the future. // Covert to a string so that the consumer because we should treat it as a black box and not // rely on it being a number. id: String(item.id), presentationId: item.presentation_id, playlistId: item.playlist_id, // Handle the case where resource is null. This should never happen unless // someone is messing around with the DB manually. presentation: item.presentation?.resource ? mapPresentationResponse(item.presentation) : null, playlist: item.playlist?.resource ? mapPlaylistResponse(item.playlist) : null, order: item.order, }); export const mapPlaylistResponse = (pl: PL.PlaylistResponse): PL.Playlist => ({ id: pl.id, name: pl.name, description: pl.description, autoRemoveOnScheduleEnd: pl.auto_remove_on_schedule_end ?? false, tzid: pl.tzid, startDatetime: pl.start_datetime ? pl.start_datetime.replace(/Z/, '') : null, endDatetime: pl.end_datetime ? pl.end_datetime.replace(/Z/, '') : null, recurrenceRule: pl.recurrence_rule, scheduleType: pl.schedule_type, items: (pl.items || []).map(mapPlaylistItemResponse), isRuleOnItems: pl.is_rule_on_items, rule: pl.rule, resource: mapResourceResponse(pl.resource), }); export const mapPlaylistV2Response = ( pl: PL.PlaylistV2Response, ): PL.PlaylistV2 => ({ id: pl.id, name: pl.name, mode: pl.mode, itemsPerPass: pl.items_per_pass, tzid: pl.tzid, startDatetime: pl.start_datetime, endDatetime: pl.end_datetime, recurrenceRule: pl.recurrence_rule, parentPlaylistId: pl.parent_playlist_id, scheduleType: pl.schedule_type, rule: pl.rule, isRuleOnItems: pl.is_rule_on_items, r: { resource: mapResourceV2Response(pl.r.resource), playlistItems: (pl.r.playlist_items || []).map((item) => ({ id: String(item.id), presentationId: item.item_presentation_id, playlistId: item.item_playlist_id, order: item.order, createdAt: item.created_at, })), }, }); export default class PlaylistClient { async getPlaylists( this: ApiClient, filter?: { ids: string[] }, ): Promise { const isValidQueryParams = filter && filter.ids.length > 0; const filterQs = isValidQueryParams ? `?id=${filter?.ids.join(',')}` : ''; const data = await this.requestProtected< PL.GetPlaylistsRequest, PL.GetPlaylistsResponse >({ method: 'GET', url: `${urls.playlists()}${filterQs}`, }); return data.map(mapPlaylistResponse); } async createPlaylist( this: ApiClient, playlist: PL.CreatePlaylist, ): Promise { const data = await this.requestProtected< PL.CreatePlaylistRequest, PL.CreatePlaylistResponse >({ method: 'POST', url: urls.playlists(), body: { name: playlist.name, description: playlist.description, schedule_type: playlist.scheduleType, auto_remove_on_schedule_end: playlist.autoRemoveOnScheduleEnd, tzid: playlist.tzid, start_datetime: playlist.startDatetime, end_datetime: playlist.endDatetime, recurrence_rule: playlist.recurrenceRule, items: playlist.items ? playlist.items.map((item) => ({ presentation_id: item.presentationId, playlist_id: item.playlistId, })) : null, is_rule_on_items: playlist.isRuleOnItems, rule: playlist.rule, resource: playlist.resource && { r: playlist.resource.r && { tags: playlist.resource.r.tags?.map(mapCreateTag), }, }, }, }); return mapPlaylistResponse(data); } async updatePlaylist( this: ApiClient, playlistId: string, playlist: PL.UpdatePlaylist, ): Promise { const data = await this.requestProtected< PL.UpdatePlaylistRequest, PL.UpdatePlaylistResponse >({ method: 'PATCH', url: urls.playlist(playlistId), body: { name: playlist.name, description: playlist.description, schedule_type: playlist.scheduleType, auto_remove_on_schedule_end: playlist.autoRemoveOnScheduleEnd, tzid: playlist.tzid, start_datetime: playlist.startDatetime, end_datetime: playlist.endDatetime, recurrence_rule: playlist.recurrenceRule, items: playlist.items?.map((item) => ({ presentation_id: item.presentationId, playlist_id: item.playlistId, })), is_rule_on_items: playlist.isRuleOnItems, rule: playlist.rule, resource: playlist.resource && { r: playlist.resource?.r && { tags: playlist.resource.r.tags?.map(mapCreateTag), }, }, }, }); return mapPlaylistResponse(data); } async deletePlaylist(this: ApiClient, playlistId: string): Promise { const data = await this.requestProtected< PL.DeletePlaylistRequest, PL.DeletePlaylistResponse >({ method: 'DELETE', url: urls.playlist(playlistId), }); return data; } async movePlaylistToFolder( this: ApiClient, playlistId: string, folderId: string | null, ): Promise { await this.requestProtected<{ folder_id: string | null }, void>({ method: 'POST', url: urls.playlistMoveToFolder(playlistId), body: { folder_id: folderId, }, }); } async getPlaylistPlaybackContent( this: ApiClient, playlistId: string, ): Promise { const data = await this.requestProtected< void, PL.PlaylistPlaybackContentResponse >({ method: 'GET', url: urls.playlistPlaybackContent(playlistId), }); return { playlistId: data.playlist_id, playlists: data.playlists && mapObjectValues(data.playlists, mapPlaylistV2Response), presentations: data.presentations && mapObjectValues(data.presentations, mapPresentationV2Response), applicationVersions: data.application_versions && mapObjectValues( data.application_versions, mapApplicationVersionResponse, ), themes: data.themes && mapObjectValues(data.themes, mapThemeV2Response), applications: data.applications && mapObjectValues(data.applications, mapApplicationV2Response), }; } async copyPlaylist( this: ApiClient, playlistId: string, params: { name?: string; targetFolderId?: string; copyOutOfTreeUnowned?: boolean; } = {}, ): Promise { const data = await this.requestProtected< PL.CopyPlaylistRequest, PL.CopyPlaylistResponse >({ method: 'POST', url: urls.playlistCopy(playlistId), body: { name: params.name, ...(params.targetFolderId ? { target_folder_id: params.targetFolderId } : {}), ...(params.copyOutOfTreeUnowned !== undefined ? { copy_out_of_tree_unowned: params.copyOutOfTreeUnowned } : {}), }, }); // Preflight: when the server flags out-of-tree unowned content it copies // nothing and returns only the flag (no playlist — so no `id` — to map), so // short-circuit. A confirmed copy always returns the playlist (even if it // echoes the flag), so the `'id' in data` guard keeps it on the real branch. if (!('id' in data)) { return { hasOutOfTreeUnowned: true, supportingContentFolderCreated: false, }; } return { ...mapPlaylistResponse(data), supportingContentFolderCreated: !!data.supporting_content_folder_created, }; } }