Skip to content
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
7 changes: 3 additions & 4 deletions apps/backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { parse as parse_yaml } from 'yaml';
import swaggerContent from './app.yaml';
import { dj_route } from './routes/djs.route.js';
import { flowsheet_route } from './routes/flowsheet.route.js';
import { flowsheet_v2_route } from './routes/flowsheet.v2.route.js';
import { library_route } from './routes/library.route.js';
import { schedule_route } from './routes/schedule.route.js';
import { events_route } from './routes/events.route.js';
Expand Down Expand Up @@ -39,6 +40,8 @@ app.use('/library', library_route);

app.use('/flowsheet', flowsheet_route);

app.use('/v2/flowsheet', flowsheet_v2_route);

app.use('/djs', dj_route);

app.use('/request', request_line_route);
Expand Down Expand Up @@ -73,7 +76,3 @@ const server = app.listen(port, () => {
});

server.setTimeout(5000);

// Force rebuild for CI

// Force rebuild for CI
1 change: 1 addition & 0 deletions apps/backend/controllers/flowsheet.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface IFSEntryMetadata {
}

export interface IFSEntry extends FSEntry {
entry_type: string;
rotation_play_freq: string | null;
metadata: IFSEntryMetadata;
}
Expand Down
33 changes: 33 additions & 0 deletions apps/backend/controllers/flowsheet.v2.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RequestHandler } from 'express';
import * as flowsheet_service from '../services/flowsheet.service.js';

/**
* GET /v2/flowsheet/playlist
* Get show info with entries in discriminated union format
*/
export const getShowInfo: RequestHandler<object, unknown, object, { show_id: string }> = async (req, res, next) => {
const showId = parseInt(req.query.show_id);

if (isNaN(showId)) {
res.status(400).json({ message: 'Missing or invalid show_id parameter' });
return;
}

try {
const showInfo = await flowsheet_service.getPlaylist(showId);

// Transform entries to V2 discriminated union format
const v2Entries = showInfo.entries.map((entry) =>
flowsheet_service.transformToV2(entry as Parameters<typeof flowsheet_service.transformToV2>[0])
);

res.status(200).json({
...showInfo,
entries: v2Entries,
});
} catch (e) {
console.error('Error: Failed to retrieve playlist');
console.error(e);
next(e);
}
};
54 changes: 40 additions & 14 deletions apps/backend/middleware/legacy/flowsheet.mirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,25 +150,50 @@ const getAddEntrySQL = async (req: Request, entry: FSEntry) => {
AND STOP_TIME = 0;`
);

if (entry?.message && entry.message.trim() !== "") {
let message = entry.message.trim();
// Determine legacy entry type code based on entry_type field (if available) or message patterns
// Legacy type codes: 0-4=rotation tracks, 6=library, 7=talkset, 8=breakpoint, 9=start, 10=end
const entryType = entry.entry_type;

// Non-track entries (messages, events, etc.)
if (entryType === 'show_start' || entryType === 'show_end' ||
entryType === 'dj_join' || entryType === 'dj_leave' ||
entryType === 'talkset' || entryType === 'breakpoint' || entryType === 'message' ||
(entry?.message && entry.message.trim() !== "" && entryType !== 'track')) {

let message = entry.message?.trim() ?? '';
let entryTypeCode = 7; // Default to talkset
let nowPlayingFlag = 0;
let startTime = 0;

// Detect the type of message entry
if (message.toLowerCase().includes("breakpoint")) {
entryTypeCode = 8; // Breakpoint
message = message.toUpperCase();
} else if (message.toLowerCase().includes("start of show") || message.toLowerCase().includes("signed on")) {
entryTypeCode = 9; // Start of show

// Map entry_type to legacy type codes
if (entryType === 'show_start') {
entryTypeCode = 9;
startTime = startMs;
} else if (message.toLowerCase().includes("end of show") || message.toLowerCase().includes("signed off")) {
entryTypeCode = 10; // End of show
} else if (entryType === 'show_end') {
entryTypeCode = 10;
startTime = startMs;
} else {
// Talkset - format as "------ talkset -------"
} else if (entryType === 'dj_join' || entryType === 'dj_leave') {
entryTypeCode = 7; // Map to talkset in legacy
} else if (entryType === 'talkset' || entryType === 'message') {
entryTypeCode = 7;
message = "------ talkset -------";
} else if (entryType === 'breakpoint') {
entryTypeCode = 8;
message = message.toUpperCase() || 'BREAKPOINT';
} else {
// Fallback to pattern matching for backwards compatibility
if (message.toLowerCase().includes("breakpoint")) {
entryTypeCode = 8;
message = message.toUpperCase();
} else if (message.toLowerCase().includes("start of show") || message.toLowerCase().includes("signed on")) {
entryTypeCode = 9;
startTime = startMs;
} else if (message.toLowerCase().includes("end of show") || message.toLowerCase().includes("signed off")) {
entryTypeCode = 10;
startTime = startMs;
} else {
message = "------ talkset -------";
}
}

statements.push(
Expand Down Expand Up @@ -201,6 +226,7 @@ const getAddEntrySQL = async (req: Request, entry: FSEntry) => {
'');` // BMI_COMPOSER
);
} else {
// Track entries
// Determine entry type code based on rotation and library IDs
// Type codes: 1-4 for different rotation types, 6 for library, 0 for manual/unknown
let entryTypeCode = 0;
Expand All @@ -211,7 +237,7 @@ const getAddEntrySQL = async (req: Request, entry: FSEntry) => {
} else if (entry.album_id && entry.album_id > 0) {
entryTypeCode = 6; // Library entry
}

statements.push(
`INSERT INTO ${FLOWSHEET_ENTRY_TABLE}
(ID, ARTIST_NAME, ARTIST_ID, SONG_TITLE, RELEASE_TITLE, RELEASE_FORMAT_ID,
Expand Down
7 changes: 7 additions & 0 deletions apps/backend/routes/flowsheet.v2.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Router } from "express";
import * as flowsheetV2Controller from "../controllers/flowsheet.v2.controller.js";

export const flowsheet_v2_route = Router();

// V2 playlist returns entries in discriminated union format
flowsheet_v2_route.get("/playlist", flowsheetV2Controller.getShowInfo);
118 changes: 118 additions & 0 deletions apps/backend/services/flowsheet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FSEntryFieldsRaw = {
id: flowsheet.id,
show_id: flowsheet.show_id,
album_id: flowsheet.album_id,
entry_type: flowsheet.entry_type,
artist_name: flowsheet.artist_name,
album_title: flowsheet.album_title,
track_title: flowsheet.track_title,
Expand Down Expand Up @@ -74,6 +75,7 @@ type FSEntryRaw = {
id: number;
show_id: number | null;
album_id: number | null;
entry_type: string;
artist_name: string | null;
album_title: string | null;
track_title: string | null;
Expand Down Expand Up @@ -101,6 +103,7 @@ const transformToIFSEntry = (raw: FSEntryRaw): IFSEntry => ({
id: raw.id,
show_id: raw.show_id,
album_id: raw.album_id,
entry_type: raw.entry_type,
artist_name: raw.artist_name,
album_title: raw.album_title,
track_title: raw.track_title,
Expand Down Expand Up @@ -290,6 +293,7 @@ export const startShow = async (dj_id: string, show_name?: string, specialty_id?

await db.insert(flowsheet).values({
show_id: new_show[0].id,
entry_type: 'show_start',
message: `Start of Show: DJ ${dj_info.djName || dj_info.name} joined the set at ${new Date().toLocaleString('en-US', {
timeZone: 'America/New_York',
})}`,
Expand Down Expand Up @@ -349,6 +353,7 @@ const createJoinNotification = async (id: string, show_id: number): Promise<FSEn
.insert(flowsheet)
.values({
show_id: show_id,
entry_type: 'dj_join',
message: message,
})
.returning();
Expand Down Expand Up @@ -382,6 +387,7 @@ export const endShow = async (currentShow: Show): Promise<Show> => {

await db.insert(flowsheet).values({
show_id: currentShow.id,
entry_type: 'show_end',
message: `End of Show: ${dj_name} left the set at ${new Date().toLocaleString('en-US', {
timeZone: 'America/New_York',
})}`,
Expand Down Expand Up @@ -426,6 +432,7 @@ const createLeaveNotification = async (dj_id: string, show_id: number): Promise<
.insert(flowsheet)
.values({
show_id: show_id,
entry_type: 'dj_leave',
message: message,
})
.returning();
Expand Down Expand Up @@ -567,3 +574,114 @@ export const getPlaylist = async (show_id: number): Promise<ShowInfo> => {
entries: entries,
};
};

/**
* Transform a V1 flowsheet entry to V2 discriminated union format.
* Removes irrelevant fields based on entry_type for cleaner API responses.
*/
export const transformToV2 = (entry: IFSEntry): Record<string, unknown> => {
const baseFields = {
id: entry.id,
show_id: entry.show_id,
play_order: entry.play_order,
add_time: entry.add_time,
entry_type: entry.entry_type,
};

switch (entry.entry_type) {
case 'track':
return {
...baseFields,
album_id: entry.album_id,
rotation_id: entry.rotation_id,
artist_name: entry.artist_name,
album_title: entry.album_title,
track_title: entry.track_title,
record_label: entry.record_label,
request_flag: entry.request_flag,
rotation_play_freq: entry.rotation_play_freq,
artwork_url: entry.artwork_url,
discogs_url: entry.discogs_url,
release_year: entry.release_year,
spotify_url: entry.spotify_url,
apple_music_url: entry.apple_music_url,
youtube_music_url: entry.youtube_music_url,
bandcamp_url: entry.bandcamp_url,
soundcloud_url: entry.soundcloud_url,
artist_bio: entry.artist_bio,
artist_wikipedia_url: entry.artist_wikipedia_url,
};

case 'show_start':
case 'show_end': {
// Parse DJ name and timestamp from message
// Format: "Start of Show: DJ {name} joined the set at {timestamp}"
// Format: "End of Show: {name} left the set at {timestamp}"
const message = entry.message || '';
let dj_name = '';
let timestamp = '';

if (entry.entry_type === 'show_start') {
const match = message.match(/^Start of Show: DJ (.+) joined the set at (.+)$/);
if (match) {
dj_name = match[1];
timestamp = match[2];
}
} else {
const match = message.match(/^End of Show: (.+) left the set at (.+)$/);
if (match) {
dj_name = match[1];
timestamp = match[2];
}
}

return {
...baseFields,
dj_name,
timestamp,
};
}

case 'dj_join':
case 'dj_leave': {
// Parse DJ name from message
// Format: "{name} joined the set!" or "{name} left the set!"
const message = entry.message || '';
let dj_name = '';

if (entry.entry_type === 'dj_join') {
const match = message.match(/^(.+) joined the set!$/);
if (match) {
dj_name = match[1];
}
} else {
const match = message.match(/^(.+) left the set!$/);
if (match) {
dj_name = match[1];
}
}

return {
...baseFields,
dj_name,
};
}

case 'talkset':
case 'message':
return {
...baseFields,
message: entry.message,
};

case 'breakpoint':
return {
...baseFields,
message: entry.message,
};

default:
// Fallback for unknown types - return all fields
return entry as Record<string, unknown>;
}
};
3 changes: 2 additions & 1 deletion shared/database/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./client.js";
export * from "./schema.js";
export * from "./schema.js";
export * from "./types/index.js";
34 changes: 34 additions & 0 deletions shared/database/src/migrations/0024_flowsheet_entry_type.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- Create flowsheet entry type enum
CREATE TYPE "wxyc_schema"."flowsheet_entry_type" AS ENUM (
'track',
'show_start',
'show_end',
'dj_join',
'dj_leave',
'talkset',
'breakpoint',
'message'
);

-- Add entry_type column (nullable initially for backfill)
ALTER TABLE "wxyc_schema"."flowsheet"
ADD COLUMN "entry_type" "wxyc_schema"."flowsheet_entry_type";

-- Backfill existing data based on message patterns
UPDATE "wxyc_schema"."flowsheet"
SET entry_type = CASE
WHEN message IS NULL OR message = '' THEN 'track'::"wxyc_schema"."flowsheet_entry_type"
WHEN message LIKE 'Start of Show:%' THEN 'show_start'::"wxyc_schema"."flowsheet_entry_type"
WHEN message LIKE 'End of Show:%' THEN 'show_end'::"wxyc_schema"."flowsheet_entry_type"
WHEN message LIKE '% joined the set!' THEN 'dj_join'::"wxyc_schema"."flowsheet_entry_type"
WHEN message LIKE '% left the set!' THEN 'dj_leave'::"wxyc_schema"."flowsheet_entry_type"
ELSE 'message'::"wxyc_schema"."flowsheet_entry_type" -- Legacy messages default to 'message' type
END;

-- Make NOT NULL with default for new entries
ALTER TABLE "wxyc_schema"."flowsheet"
ALTER COLUMN "entry_type" SET NOT NULL,
ALTER COLUMN "entry_type" SET DEFAULT 'track';

-- Add index for filtering by entry type
CREATE INDEX "flowsheet_entry_type_idx" ON "wxyc_schema"."flowsheet" ("entry_type");
Loading
Loading