Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stream title template functionality #27

Merged
merged 1 commit into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ VITE_TWITCH_CLIENT_ID=???
VITE_TWITCH_REDIRECT_URI="http://localhost:5173/twitch-callback"

VITE_API_URL="http://localhost:5173/api/"
WEBSOCKET_URL="ws://localhost:5173"
WEBSOCKET_URL="ws://localhost:5173"
MOCKS_ENABLED=true
11 changes: 6 additions & 5 deletions src/components/molecules/StreamInfoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TwitchCCTAutocomplete from '@/components/atoms/TwitchCCLSelect';
import TwitchCategoryAutocomplete from '@/components/atoms/TwitchCategoryAutocomplete';
import type { Profile } from '@/hooks/useProfile';
import type { StreamEvent } from '@/scheduling/types';
import { templateStreamSeries } from '@/utilities/template';
import {
type GetChannelInformationResponse,
getChannelInformation,
Expand Down Expand Up @@ -91,17 +92,17 @@ function StreamInfoEditor({

const newTags = isMerging
? [
...nextScheduledStream.tags,
...(nextScheduledStream.tags || []),
...profile.standardTags,
...(streamInfo.tags || []),
]
: [...nextScheduledStream.tags, ...profile.standardTags];
: [...(nextScheduledStream.tags || []), ...profile.standardTags];

setStreamInfo((streamInfo) => ({
...streamInfo,
game_id: nextScheduledStream.twitch_category.id,
game_name: nextScheduledStream.twitch_category.name,
title: nextScheduledStream.name,
game_id: nextScheduledStream.twitch_category?.id,
game_name: nextScheduledStream.twitch_category?.name,
title: templateStreamSeries(nextScheduledStream),
tags: newTags,
}));
};
Expand Down
9 changes: 6 additions & 3 deletions src/components/molecules/UpcomingStream.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Profile } from '@/hooks/useProfile';
import type { StreamEvent } from '@/scheduling/types';
import { templateStreamSeries } from '@/utilities/template';
import Alert from '@mui/material/Alert';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
Expand Down Expand Up @@ -44,7 +45,7 @@ function UpcomingStream({ nextScheduledStream, profile }: UpcomingStreamProps) {
.split(', ');

const tags = Array.from(
new Set([...nextScheduledStream.tags, ...profile.standardTags]),
new Set([...(nextScheduledStream.tags || []), ...profile.standardTags]),
).sort();

return (
Expand All @@ -54,7 +55,9 @@ function UpcomingStream({ nextScheduledStream, profile }: UpcomingStreamProps) {
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6">{nextScheduledStream.name}</Typography>
<Typography variant="h6">
{templateStreamSeries(nextScheduledStream)}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="textSecondary">
Expand Down Expand Up @@ -89,7 +92,7 @@ function UpcomingStream({ nextScheduledStream, profile }: UpcomingStreamProps) {
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">
Category: {nextScheduledStream.twitch_category.name}
Category: {nextScheduledStream.twitch_category?.name}
</Typography>
</Grid>
<Grid item xs={12}>
Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/StreamManagerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Timers from '@/components/molecules/Timers';
import UpcomingStream from '@/components/molecules/UpcomingStream';
import useProfile from '@/hooks/useProfile';
import findNextStream from '@/scheduling/findNextStream';
import type { SeriesRecord } from '@/types/dataProvider';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
Expand All @@ -28,7 +29,7 @@ function StreamManagerPage() {
error: streamPlanError,
isLoading: streamPlanIsLoading,
isError: streamPlanIsError,
} = useGetList('stream_plans', {});
} = useGetList<SeriesRecord>('series', {});

// get the user profile
const {
Expand Down
6 changes: 6 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { createRoot } from 'react-dom/client';

import App from '@/App.tsx';

const { VITE_MOCKS_ENABLED: MOCKS_ENABLED } = import.meta.env;

const container = document.getElementById('root');

if (!container) {
Expand All @@ -13,6 +15,10 @@ async function enableMocking() {
return;
}

if (!MOCKS_ENABLED) {
return;
}

const { worker } = await import('./mocks/browser');

// `worker.start()` returns a Promise that resolves
Expand Down
13 changes: 13 additions & 0 deletions src/resources/stream_plans/StreamPlansEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ function StreamPlansEdit() {
<TextInput source="title" validate={required()} />
<TextInput source="description" />
<BooleanInput source="is_active" />

<TextInput
source="stream_title_template"
validate={required()}
helperText="Template for the title of the stream. Use ${title} to insert the series title and ${stream_count} to insert the stream number"
/>
<NumberInput
source="stream_count"
helperText="Number of streams that have been done for this series"
validate={required()}
defaultValue={0}
/>

<RichTextInput source="prep_notes" />

<DateInput
Expand Down
19 changes: 16 additions & 3 deletions src/scheduling/generateEventsForDay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { RECURRENCE_DAYS, type SeriesRecord } from '@/types/dataProvider';
import { DateTime } from 'luxon';
import { RECURRENCE_DAYS, type StreamEvent, type StreamPlan } from './types';
import type { SeriesRecordWithValidTimes, StreamEvent } from './types';

function guardValidSeriesRecord(
record: SeriesRecord,
): record is SeriesRecordWithValidTimes {
return (
typeof record.start_time === 'string' &&
typeof record.end_time === 'string' &&
typeof record.start_date === 'string' &&
typeof record.timezone === 'string'
);
}

/**
* Generate events for a given day based on the stream plans data, including
Expand All @@ -11,10 +23,11 @@ import { RECURRENCE_DAYS, type StreamEvent, type StreamPlan } from './types';
*/
export default function generateEventsForDay(
targetDate: DateTime,
plans: StreamPlan[],
plans: SeriesRecord[],
): StreamEvent[] {
return (
plans
.filter(guardValidSeriesRecord)
// we are assuming that the recurrence cannot be more than once per day
// so we can filter the list and only include the events that fall on the
// given day, rather than having to potentially generate multiple events
Expand Down Expand Up @@ -61,7 +74,7 @@ export default function generateEventsForDay(
}

// validate the recurrence type
if (plan.recurrence.type !== 'weekly') {
if (plan.recurrence?.type !== 'weekly') {
throw new Error('Unsupported recurrence type');
}

Expand Down
52 changes: 5 additions & 47 deletions src/scheduling/types.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,14 @@
import type { TwitchCategory } from '@/utilities/twitch';
import type { SeriesRecord } from '@/types/dataProvider';
import type { DateTime } from 'luxon';

export interface Skip {
date: string;
reason: string;
}

export type RecurrenceType = 'weekly';
export type RecurrenceDay =
| 'sunday'
| 'monday'
| 'tuesday'
| 'wednesday'
| 'thursday'
| 'friday'
| 'saturday';

export const RECURRENCE_DAYS: Record<number, RecurrenceDay> = {
7: 'sunday',
1: 'monday',
2: 'tuesday',
3: 'wednesday',
4: 'thursday',
5: 'friday',
6: 'saturday',
};

export interface Recurrence {
type: RecurrenceType;
days: RecurrenceDay[];
// interval should be a positive integer
interval: number;
}

export interface StreamPlan {
id: number;
name: string;
description: string;
prep_notes: string;
start_date: string;
end_date: string;
skips?: Skip[];
recurrence: Recurrence;
timezone: string;
export interface SeriesRecordWithValidTimes extends SeriesRecord {
start_time: string;
end_time: string;
tags: string[];
category: string;
twitch_category: TwitchCategory;
start_date: string;
timezone: string;
}

export interface StreamEvent extends StreamPlan {
export interface StreamEvent extends SeriesRecord {
startDatetime: DateTime;
endDatetime: DateTime;
}
57 changes: 57 additions & 0 deletions src/types/dataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { TwitchCategory } from '@/utilities/twitch';
import type { Identifier, RaRecord } from 'react-admin';

export interface TranscriptSegment {
Expand Down Expand Up @@ -95,3 +96,59 @@ export interface EpisodeRecord extends RaRecord {
tracks: Array<{ start: string; end: string }>;
stream_id: Identifier;
}

export interface Skip {
date: string;
reason: string;
}

export type RecurrenceType = 'weekly';
export type RecurrenceDay =
| 'sunday'
| 'monday'
| 'tuesday'
| 'wednesday'
| 'thursday'
| 'friday'
| 'saturday';

export const RECURRENCE_DAYS: Record<number, RecurrenceDay> = {
7: 'sunday',
1: 'monday',
2: 'tuesday',
3: 'wednesday',
4: 'thursday',
5: 'friday',
6: 'saturday',
};

export interface Recurrence {
type: RecurrenceType;
days: RecurrenceDay[];
// interval should be a positive integer
interval: number;
}

export interface SeriesRecord extends RaRecord {
category?: number;
created_at: string;
description?: string;
is_active?: boolean;
max_episode_order_index?: number;
notify_subscribers?: boolean;
playlist_id?: string;
prep_notes?: string;
skips?: Skip[];
recurrence?: Recurrence;
timezone?: string;
start_time?: string;
end_time?: string;
start_date?: string;
tags?: string[];
thumbnail_url?: string;
title: string;
stream_title_template?: string;
twitch_category?: TwitchCategory;
updated_at?: string;
stream_count?: number;
}
16 changes: 16 additions & 0 deletions src/utilities/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { SeriesRecord } from '@/types/dataProvider';

export default function template(str: string, data: Record<string, string>) {
return str.replace(/\${(.*?)}/g, (_match, key) => data[key] || '');
}

export function templateStreamSeries(series: SeriesRecord): string {
if (series.stream_title_template) {
return template(series.stream_title_template, {
title: series.title,
stream_count: `${series.stream_count || 1}`,
});
}

return 'No title template set';
}
9 changes: 8 additions & 1 deletion src/utilities/twitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ export async function getChannelInformation(
}

return {
...json.data[0],
broadcaster_id: json.data[0].broadcaster_id,
broadcaster_login: json.data[0].broadcaster_login,
broadcaster_name: json.data[0].broadcaster_name,
broadcaster_language: json.data[0].broadcaster_language,
title: json.data[0].title,
tags: json.data[0].tags,
game_name: json.data[0].game_name,
is_branded_content: json.data[0].is_branded_content,

game_id: json.data[0].game_id || null,

Expand Down
Loading