From b2f9080c56507729373ad51735ade40a3d3c980d Mon Sep 17 00:00:00 2001
From: Konrad Mohrfeldt <km@roko.li>
Date: Tue, 23 Jul 2024 00:46:25 +0200
Subject: [PATCH] feat: add new program store

This store exposes a continuous program that is broadcasted for a radio.

refs #128
---
 src/steering-types.ts | 292 ++++++++++++++++++++++++++++--------------
 src/stores/index.ts   |   1 +
 src/stores/program.ts |  64 +++++++++
 src/types.ts          |   1 +
 4 files changed, 265 insertions(+), 93 deletions(-)
 create mode 100644 src/stores/program.ts

diff --git a/src/steering-types.ts b/src/steering-types.ts
index 45cf7ef1..4de15b5b 100644
--- a/src/steering-types.ts
+++ b/src/steering-types.ts
@@ -1,6 +1,6 @@
 /* eslint-disable */
 /*
- * This file was auto-generated by `make update-types` at 2024-07-11 11:45:59.605Z.
+ * This file was auto-generated by `make update-types` at 2024-07-22 22:29:03.645Z.
  * DO NOT make changes to this file.
  */
 
@@ -156,13 +156,6 @@ export interface paths {
      */
     patch: operations['notes_partial_update']
   }
-  '/api/v1/playout/': {
-    /**
-     * List scheduled playout.
-     * @description Returns a list of the scheduled playout. The schedule will include virtual timeslots to fill unscheduled gaps if requested.
-     */
-    get: operations['playout_list']
-  }
   '/api/v1/profiles/': {
     /** List all profiles. */
     get: operations['profiles_list']
@@ -179,19 +172,17 @@ export interface paths {
     /** Partially update an existing profile. */
     patch: operations['profiles_partial_update']
   }
-  '/api/v1/program/{year}/{month}/{day}/': {
-    /**
-     * List schedule for a specific date.
-     * @description Returns a list of the schedule for a specific date.Expects parameters `year` (int), `month` (int), and `day` (int) as url components.e.g. /program/2024/01/31/
-     */
-    get: operations['program_list']
+  '/api/v1/program/basic/': {
+    /** List program for a specific date range. Only returns the most basic data for clients that fetch other data themselves. */
+    get: operations['program_basic_list']
   }
-  '/api/v1/program/week/': {
-    /**
-     * List scheduled playout.
-     * @description Returns a list of the scheduled playout. The schedule will include virtual timeslots to fill unscheduled gaps if requested.
-     */
-    get: operations['program_week_list']
+  '/api/v1/program/calendar/': {
+    /** List program for a specific date range. Returns all relevant data for the specified time frame. */
+    get: operations['program_calendar_list']
+  }
+  '/api/v1/program/playout/': {
+    /** List program for a specific date range. Returns an extended program dataset for use in the AURA engine. Not recommended for other tools. */
+    get: operations['program_playout_list']
   }
   '/api/v1/rrules/': {
     /** List all rrules. */
@@ -369,6 +360,16 @@ export type webhooks = Record<string, never>
 
 export interface components {
   schemas: {
+    BasicProgramEntry: {
+      id: string
+      /** Format: date-time */
+      start: string
+      /** Format: date-time */
+      end: string
+      timeslotId: number | null
+      playlistId: number | null
+      showId: number
+    }
     /**
      * @description * `1` - first
      * * `2` - second
@@ -419,6 +420,128 @@ export interface components {
       updatedAt: string | null
       updatedBy: string
     }
+    CalendarEpisode: {
+      /** @description CBA entry ID. */
+      cbaId?: number | null
+      /** @description Textual content of the note. */
+      content: string
+      /** @description `Profile` IDs that contributed to this episode. */
+      contributorIds?: number[]
+      id: number
+      /** @description `Image` ID. */
+      imageId?: number | null
+      /** @description Array of `Language` IDs. */
+      languageIds?: (number | null)[]
+      /** @description Array of `Link` objects. */
+      links?: components['schemas']['NoteLink'][]
+      /** @description Array of `Playlist` IDs. */
+      playlistId?: number
+      /** @description Summary of the Note. */
+      summary?: string
+      /** @description Tags of the Note. */
+      tags?: string
+      /** @description `Timeslot` ID. */
+      timeslotId?: number
+      /** @description Title of the note. */
+      title?: string
+      /** @description Array of `Topic`IDs. */
+      topicIds?: (number | null)[]
+    }
+    CalendarProfile: {
+      /** @description Biography of the profile. */
+      biography?: string
+      /**
+       * Format: email
+       * @description Email address of the profile.
+       */
+      email?: string
+      id: number
+      /** @description `Image` id of the profile. */
+      imageId?: number | null
+      /** @description True if the profile is active. */
+      isActive?: boolean
+      /** @description Array of `Link` objects. Can be empty. */
+      links?: components['schemas']['ProfileLink'][]
+      /** @description Display name of the profile. */
+      name: string
+    }
+    CalendarSchema: {
+      shows: components['schemas']['CalendarShow'][]
+      timeslots: components['schemas']['CalendarTimeslot'][]
+      profiles: components['schemas']['CalendarProfile'][]
+      categories: components['schemas']['Category'][]
+      fundingCategories: components['schemas']['FundingCategory'][]
+      types: components['schemas']['Type'][]
+      images: components['schemas']['Image'][]
+      topics: components['schemas']['Topic'][]
+      languages: components['schemas']['Language'][]
+      musicFocuses: components['schemas']['MusicFocus'][]
+      program: components['schemas']['BasicProgramEntry'][]
+      episodes: components['schemas']['CalendarEpisode'][]
+      licenses: components['schemas']['License'][]
+      linkTypes: components['schemas']['LinkType'][]
+    }
+    CalendarShow: {
+      /** @description Array of `Category` IDs. */
+      categoryIds: number[]
+      /** @description CBA series ID. */
+      cbaSeriesId?: number | null
+      /** @description Default `Playlist` ID for this show. */
+      defaultPlaylistId?: number | null
+      /** @description Description of this show. */
+      description?: string
+      /**
+       * Format: email
+       * @description Email address of this show.
+       */
+      email?: string | null
+      /** @description `FundingCategory` ID. */
+      fundingCategoryId: number
+      /** @description `Profile` IDs that host this show. */
+      hostIds: number[]
+      id: number
+      /** @description `Image` ID of this show. */
+      imageId?: number | null
+      /** @description True if this show is active. */
+      isActive?: boolean
+      /** @description True if this show is public. */
+      isPublic?: boolean
+      /** @description `Language` IDs of this show. */
+      languageIds: number[]
+      /** @description Array of `Link` objects. */
+      links?: components['schemas']['ProfileLink'][]
+      /** @description `Image` ID of the logo of this show. */
+      logoId?: number | null
+      /** @description Array of `MusicFocus` IDs. */
+      musicFocusIds: number[]
+      /** @description Name of this Show. */
+      name: string
+      /** @description `Show` ID that predeceeded this one. */
+      predecessorId?: number | null
+      /** @description Short description of this show. */
+      shortDescription: string
+      /** @description Slug of this show. */
+      slug?: string
+      /** @description Array of `Topic` IDs. */
+      topicIds: number[]
+      /** @description Array of `Type` IDs. */
+      typeId: number
+    }
+    CalendarTimeslot: {
+      /** @description Playlist ID of this timeslot. */
+      playlistId?: number | null
+      /** @description This timeslot is a repetition of `Timeslot` ID. */
+      repetitionOfId?: number | null
+      /** Format: date-time */
+      end: string
+      id: number
+      noteId: number
+      /** @description `Schedule` ID of this timeslot. */
+      scheduleId?: number
+      showId: number
+      /** Format: date-time */
+      start: string
+    }
     Category: {
       /** @description Description of the category. */
       description?: string
@@ -541,17 +664,6 @@ export interface components {
       /** @description Slug of the music focus. */
       slug: string
     }
-    NestedTimeslot: {
-      /** Format: date-time */
-      end: string
-      id: number | null
-      isVirtual: boolean
-      memo: string
-      playlistId: number | null
-      repetitionOfId: number | null
-      /** Format: date-time */
-      start: string
-    }
     Note: {
       /** @description CBA entry ID. */
       cbaId?: number | null
@@ -991,21 +1103,35 @@ export interface components {
       permissions?: readonly string[]
       profileIds?: number[]
     }
-    PlayoutEntry: {
-      episode: {
-        readonly id: number | null
-        readonly title: string
-      }
-      schedule: {
-        readonly id: number | null
-        readonly defaultPlaylistId: number | null
-      } | null
-      show: {
-        readonly defaultPlaylistId: number | null
-        readonly id: number
-        readonly name: string
-      }
-      timeslot: components['schemas']['NestedTimeslot']
+    PlayoutEpisode: {
+      id: number
+      /** @description Title of the note. */
+      title?: string
+    }
+    PlayoutProgramEntry: {
+      id: string
+      /** Format: date-time */
+      start: string
+      /** Format: date-time */
+      end: string
+      timeslotId: number | null
+      playlistId: number | null
+      showId: number
+      timeslot: components['schemas']['TimeSlot']
+      show: components['schemas']['PlayoutShow']
+      episode: components['schemas']['PlayoutEpisode'] | null
+      schedule: components['schemas']['PlayoutSchedule'] | null
+    }
+    PlayoutSchedule: {
+      id: number
+      /** @description A tank ID in case the timeslot's playlist_id is empty. */
+      defaultPlaylistId?: number | null
+    }
+    PlayoutShow: {
+      id: number
+      /** @description Name of this Show. */
+      name: string
+      defaultPlaylistId?: number | null
     }
     Profile: {
       /** @description Biography of the profile. */
@@ -1038,18 +1164,6 @@ export interface components {
       /** Format: uri */
       url: string
     }
-    ProgramEntry: {
-      episode: {
-        readonly id: number | null
-        readonly title: string
-      }
-      show: {
-        readonly defaultPlaylistId: number | null
-        readonly id: number
-        readonly name: string
-      }
-      timeslot: components['schemas']['NestedTimeslot']
-    }
     ProjectedTimeSlot: {
       hash: string
       /** Format: date-time */
@@ -2418,25 +2532,6 @@ export interface operations {
       }
     }
   }
-  /**
-   * List scheduled playout.
-   * @description Returns a list of the scheduled playout. The schedule will include virtual timeslots to fill unscheduled gaps if requested.
-   */
-  playout_list: {
-    parameters: {
-      query?: {
-        /** @description Include virtual timeslot entries (default: false). */
-        includeVirtual?: boolean
-      }
-    }
-    responses: {
-      200: {
-        content: {
-          'application/json': components['schemas']['PlayoutEntry'][]
-        }
-      }
-    }
-  }
   /** List all profiles. */
   profiles_list: {
     parameters: {
@@ -2551,45 +2646,56 @@ export interface operations {
       }
     }
   }
-  /**
-   * List schedule for a specific date.
-   * @description Returns a list of the schedule for a specific date.Expects parameters `year` (int), `month` (int), and `day` (int) as url components.e.g. /program/2024/01/31/
-   */
-  program_list: {
+  /** List program for a specific date range. Only returns the most basic data for clients that fetch other data themselves. */
+  program_basic_list: {
     parameters: {
       query?: {
+        end?: boolean
         /** @description Include virtual timeslot entries (default: false). */
         includeVirtual?: boolean
+        start?: boolean
       }
-      path: {
-        day: number
-        month: number
-        year: number
+    }
+    responses: {
+      200: {
+        content: {
+          'application/json': components['schemas']['BasicProgramEntry'][]
+        }
+      }
+    }
+  }
+  /** List program for a specific date range. Returns all relevant data for the specified time frame. */
+  program_calendar_list: {
+    parameters: {
+      query?: {
+        end?: boolean
+        /** @description Include virtual timeslot entries (default: false). */
+        includeVirtual?: boolean
+        start?: boolean
       }
     }
     responses: {
       200: {
         content: {
-          'application/json': components['schemas']['ProgramEntry'][]
+          'application/json': components['schemas']['CalendarSchema'][]
         }
       }
     }
   }
-  /**
-   * List scheduled playout.
-   * @description Returns a list of the scheduled playout. The schedule will include virtual timeslots to fill unscheduled gaps if requested.
-   */
-  program_week_list: {
+  /** List program for a specific date range. Returns an extended program dataset for use in the AURA engine. Not recommended for other tools. */
+  program_playout_list: {
     parameters: {
       query?: {
+        end?: boolean
         /** @description Include virtual timeslot entries (default: false). */
         includeVirtual?: boolean
+        start?: boolean
       }
     }
     responses: {
       200: {
         content: {
-          'application/json': components['schemas']['PlayoutEntry'][]
+          'application/json': components['schemas']['PlayoutProgramEntry'][]
         }
       }
     }
diff --git a/src/stores/index.ts b/src/stores/index.ts
index fe509293..0b4d9baa 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -15,6 +15,7 @@ export { useScheduleStore } from '@/stores/schedules'
 export { useShowStore } from '@/stores/shows'
 export { useTimeSlotStore } from '@/stores/timeslots'
 export { useRadioSettingsStore } from '@/stores/radio-settings'
+export { useProgramStore } from '@/stores/program'
 export { useRRuleStore } from '@/stores/rrules'
 
 export const useFundingCategoryStore = createUnpaginatedAPIStore<FundingCategory>(
diff --git a/src/stores/program.ts b/src/stores/program.ts
new file mode 100644
index 00000000..9507fb64
--- /dev/null
+++ b/src/stores/program.ts
@@ -0,0 +1,64 @@
+import { ListOperationOptions } from '@rokoli/bnb'
+import { defineStore } from 'pinia'
+import { APIListUnpaginated, createExtendableAPI } from '@rokoli/bnb/drf'
+import { steeringAuthInit } from '@/stores/auth'
+import { createSteeringURL } from '@/api'
+import { ProgramEntry } from '@/types'
+import { useQuery } from '@/util'
+import { MaybeRefOrGetter, readonly, ref, toValue, watch } from 'vue'
+
+export const useProgramStore = defineStore('program', () => {
+  const endpoint = createSteeringURL.prefix('program', 'basic')
+  const { api } = createExtendableAPI<ProgramEntry>(endpoint, steeringAuthInit)
+  const { list: _list } = APIListUnpaginated(api)
+
+  return {
+    list: (options: ListOperationOptions | undefined = undefined) => {
+      return _list({ ...(options ?? {}), noStoreCommit: true })
+    },
+  }
+})
+
+interface UseProgramOptions {
+  start: MaybeRefOrGetter<string | Date>
+  end: MaybeRefOrGetter<string | Date>
+}
+
+export function useProgramSlots(options: UseProgramOptions) {
+  const programStore = useProgramStore()
+  const error = ref<Error | undefined>()
+  const isLoading = ref<boolean>(false)
+  let controller: AbortController | undefined = undefined
+  const program = ref<ProgramEntry[]>([])
+
+  const query = useQuery(() => ({
+    start: toValue(options.start),
+    end: toValue(options.end),
+    includeVirtual: true,
+  }))
+
+  async function updateProgram() {
+    if (controller) controller.abort('date changed')
+    error.value = undefined
+    controller = new AbortController()
+    isLoading.value = true
+    try {
+      program.value = await programStore.list({
+        query: query.value,
+        requestInit: { signal: controller.signal },
+      })
+    } finally {
+      isLoading.value = false
+      controller = undefined
+    }
+  }
+
+  watch(query, updateProgram, { immediate: true })
+
+  return {
+    program: readonly(program),
+    error: readonly(error),
+    isLoading: readonly(isLoading),
+    reload: updateProgram,
+  }
+}
diff --git a/src/types.ts b/src/types.ts
index bfeb8715..8d7beb39 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -56,6 +56,7 @@ export type Topic = Required<steeringComponents['schemas']['Topic']>
 export type License = Required<steeringComponents['schemas']['License']>
 export type LinkType = Required<steeringComponents['schemas']['LinkType']>
 export type Link = { typeId: LinkType['id'] | null; url: string }
+export type ProgramEntry = steeringComponents['schemas']['BasicProgramEntry']
 export type RadioSettings = Required<steeringComponents['schemas']['RadioSettings']>
 export type RRule = Required<
   Omit<steeringComponents['schemas']['RRule'], 'byWeekdays'> & {
-- 
GitLab