Skip to content
Open
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: 3 additions & 0 deletions apps/backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 { labels_route } from './routes/labels.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 @@ -36,6 +37,8 @@ const swaggerDoc = parse_yaml(swaggerContent);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc));

// Business logic routes
app.use('/labels', labels_route);

app.use('/library', library_route);

app.use('/flowsheet', flowsheet_route);
Expand Down
3 changes: 3 additions & 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 {
label_id: number | null;
rotation_play_freq: string | null;
metadata: IFSEntryMetadata;
}
Expand Down Expand Up @@ -125,6 +126,7 @@ export type FSEntryRequestBody = {
album_id?: number;
rotation_id?: number;
record_label: string;
label_id?: number;
request_flag?: boolean;
message?: string;
};
Expand Down Expand Up @@ -261,6 +263,7 @@ export type UpdateRequestBody = {
album_title?: string;
track_title?: string;
record_label?: string;
label_id?: number;
request_flag?: boolean;
message?: string;
};
Expand Down
79 changes: 79 additions & 0 deletions apps/backend/controllers/labels.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Request, RequestHandler } from 'express';
import * as labelsService from '../services/labels.service.js';

type CreateLabelRequest = {
label_name: string;
parent_label_id?: number;
};

export const getLabels: RequestHandler = async (req, res, next) => {
try {
const labels = await labelsService.getAllLabels();
res.status(200).json(labels);
} catch (e) {
console.error('Error retrieving labels');
console.error(e);
next(e);
}
};

export const getLabel: RequestHandler<object, unknown, unknown, { id: string }> = async (req, res, next) => {
const id = parseInt(req.query.id);
if (isNaN(id)) {
res.status(400).json({ message: 'Missing or invalid label id' });
} else {
try {
const label = await labelsService.getLabelById(id);
if (label) {
res.status(200).json(label);
} else {
res.status(404).json({ message: 'Label not found' });
}
} catch (e) {
console.error('Error retrieving label');
console.error(e);
next(e);
}
}
};

export const createLabel: RequestHandler = async (
req: Request<object, object, CreateLabelRequest>,
res,
next
) => {
const { body } = req;
if (!body.label_name) {
res.status(400).json({ message: 'Missing required parameter: label_name' });
} else {
try {
const label = await labelsService.createLabel(body.label_name, body.parent_label_id);
res.status(200).json(label);
} catch (e) {
console.error('Error creating label');
console.error(e);
next(e);
}
}
};

export const searchLabelsEndpoint: RequestHandler<object, unknown, unknown, { q: string; limit?: string }> = async (
req,
res,
next
) => {
const query = req.query.q;
if (!query) {
res.status(400).json({ message: 'Missing required query parameter: q' });
} else {
try {
const limit = req.query.limit ? parseInt(req.query.limit) : undefined;
const labels = await labelsService.searchLabels(query, limit);
res.status(200).json(labels);
} catch (e) {
console.error('Error searching labels');
console.error(e);
next(e);
}
}
};
10 changes: 10 additions & 0 deletions apps/backend/controllers/library.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
RotationRelease,
} from '@wxyc/database';
import * as libraryService from '../services/library.service.js';
import * as labelsService from '../services/labels.service.js';

type NewAlbumRequest = {
album_title: string;
artist_name?: string;
artist_id?: number;
alternate_artist_name?: string;
label: string;
label_id?: number;
genre_id: number;
format_id: number;
disc_quantity?: number;
Expand Down Expand Up @@ -55,12 +57,20 @@ export const addAlbum: RequestHandler = async (req: Request<object, object, NewA
);
} else {
try {
// Resolve label string to label_id via upsert
let label_id = body.label_id;
if (label_id === undefined && body.label) {
const resolvedLabel = await labelsService.createLabel(body.label);
label_id = resolvedLabel.id;
}

const new_album: NewAlbum = {
artist_id: artist_id,
genre_id: body.genre_id,
format_id: body.format_id,
album_title: body.album_title,
label: body.label,
label_id: label_id,
code_number: await libraryService.generateAlbumCodeNumber(artist_id),
alternate_artist_name: body.alternate_artist_name,
disc_quantity: body.disc_quantity,
Expand Down
29 changes: 29 additions & 0 deletions apps/backend/routes/labels.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { requirePermissions } from "@wxyc/authentication";
import { Router } from "express";
import * as labelsController from "../controllers/labels.controller.js";

export const labels_route = Router();

labels_route.get(
"/",
requirePermissions({ catalog: ["read"] }),
labelsController.getLabels
);

labels_route.get(
"/info",
requirePermissions({ catalog: ["read"] }),
labelsController.getLabel
);

labels_route.get(
"/search",
requirePermissions({ catalog: ["read"] }),
labelsController.searchLabelsEndpoint
);

labels_route.post(
"/",
requirePermissions({ catalog: ["write"] }),
labelsController.createLabel
);
5 changes: 5 additions & 0 deletions apps/backend/services/flowsheet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const FSEntryFieldsRaw = {
album_title: flowsheet.album_title,
track_title: flowsheet.track_title,
record_label: flowsheet.record_label,
label_id: flowsheet.label_id,
rotation_id: flowsheet.rotation_id,
rotation_play_freq: rotation.play_freq,
request_flag: flowsheet.request_flag,
Expand Down Expand Up @@ -80,6 +81,7 @@ type FSEntryRaw = {
album_title: string | null;
track_title: string | null;
record_label: string | null;
label_id: number | null;
rotation_id: number | null;
rotation_play_freq: string | null;
request_flag: boolean | null;
Expand Down Expand Up @@ -108,6 +110,7 @@ const transformToIFSEntry = (raw: FSEntryRaw): IFSEntry => ({
album_title: raw.album_title,
track_title: raw.track_title,
record_label: raw.record_label,
label_id: raw.label_id,
rotation_id: raw.rotation_id,
rotation_play_freq: raw.rotation_play_freq,
request_flag: raw.request_flag ?? false,
Expand Down Expand Up @@ -506,6 +509,7 @@ export const getAlbumFromDB = async (album_id: number) => {
artist_name: artists.artist_name,
album_title: library.album_title,
record_label: library.label,
label_id: library.label_id,
})
.from(library)
.innerJoin(artists, eq(artists.id, library.artist_id))
Expand Down Expand Up @@ -601,6 +605,7 @@ export const transformToV2 = (entry: IFSEntry): Record<string, unknown> => {
album_title: entry.album_title,
track_title: entry.track_title,
record_label: entry.record_label,
label_id: entry.label_id,
request_flag: entry.request_flag,
rotation_play_freq: entry.rotation_play_freq,
artwork_url: entry.metadata?.artwork_url ?? null,
Expand Down
59 changes: 59 additions & 0 deletions apps/backend/services/labels.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { eq, sql } from 'drizzle-orm';
import { db, labels, Label } from '@wxyc/database';

export const getAllLabels = async (): Promise<Label[]> => {
return await db.select().from(labels);
};

export const getLabelById = async (id: number): Promise<Label | undefined> => {
const result = await db
.select()
.from(labels)
.where(eq(labels.id, id))
.limit(1);
return result[0];
};

export const createLabel = async (
labelName: string,
parentLabelId?: number
): Promise<Label> => {
const values: { label_name: string; parent_label_id?: number } = {
label_name: labelName,
};
if (parentLabelId !== undefined) {
values.parent_label_id = parentLabelId;
}

const result = await db
.insert(labels)
.values(values)
.onConflictDoNothing({ target: labels.label_name })
.returning();

// If conflict (label already exists), fetch the existing one
if (result.length === 0) {
const existing = await db
.select()
.from(labels)
.where(eq(labels.label_name, labelName))
.limit(1);
return existing[0];
}

return result[0];
};

export const searchLabels = async (
query: string,
limit = 10
): Promise<Label[]> => {
const searchQuery = sql`
SELECT * FROM ${labels}
WHERE ${labels.label_name} ILIKE ${query + '%'}
ORDER BY ${labels.label_name}
LIMIT ${limit}
`;
const response = await db.execute(searchQuery);
return response.rows as Label[];
};
3 changes: 3 additions & 0 deletions apps/backend/services/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface Rotation {
artist_name: string;
album_title: string;
record_label: string | null;
label_id: number | null;
genre_name: string;
format_name: string;
rotation_id: number;
Expand All @@ -59,6 +60,7 @@ export const getRotationFromDB = async (): Promise<Rotation[]> => {
artist_name: artists.artist_name,
album_title: library.album_title,
record_label: library.label,
label_id: library.label_id,
genre_name: genres.genre_name,
format_name: format.format_name,
rotation_id: rotation.id,
Expand Down Expand Up @@ -197,6 +199,7 @@ export const getAlbumFromDB = async (album_id: number) => {
artist_name: artists.artist_name,
album_title: library.album_title,
record_label: library.label,
label_id: library.label_id,
plays: library.plays,
add_date: library.add_date,
last_modified: library.last_modified,
Expand Down
13 changes: 13 additions & 0 deletions shared/database/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export const library = wxyc_schema.table(
alternate_artist_name: varchar('alternate_artist_name', { length: 128 }),
album_title: varchar('album_title', { length: 128 }).notNull(),
label: varchar('label', { length: 128 }),
label_id: integer('label_id').references(() => labels.id),
code_number: smallint('code_number').notNull(),
disc_quantity: smallint('disc_quantity').default(1).notNull(),
plays: integer('plays').default(0).notNull(),
Expand Down Expand Up @@ -304,6 +305,7 @@ export const flowsheet = wxyc_schema.table('flowsheet', {
album_title: varchar('album_title', { length: 128 }),
artist_name: varchar('artist_name', { length: 128 }),
record_label: varchar('record_label', { length: 128 }),
label_id: integer('label_id').references(() => labels.id),
play_order: serial('play_order').notNull(),
request_flag: boolean('request_flag').default(false).notNull(),
message: varchar('message', { length: 250 }),
Expand All @@ -321,6 +323,14 @@ export const genres = wxyc_schema.table('genres', {
last_modified: timestamp('last_modified').defaultNow().notNull(),
});

export type NewLabel = InferInsertModel<typeof labels>;
export type Label = InferSelectModel<typeof labels>;
export const labels = wxyc_schema.table('labels', {
id: serial('id').primaryKey(),
label_name: varchar('label_name', { length: 128 }).notNull().unique(),
parent_label_id: integer('parent_label_id'),
});

export type NewReview = InferInsertModel<typeof reviews>;
export type Review = InferSelectModel<typeof reviews>;
export const reviews = wxyc_schema.table('reviews', {
Expand Down Expand Up @@ -422,6 +432,7 @@ export type LibraryArtistViewEntry = {
play_freq: string | null;
add_date: Date;
label: string | null;
label_id: number | null;
};
export const library_artist_view = wxyc_schema.view('library_artist_view').as((qb) => {
return qb
Expand All @@ -437,6 +448,7 @@ export const library_artist_view = wxyc_schema.view('library_artist_view').as((q
play_freq: rotation.play_freq,
add_date: library.add_date,
label: library.label,
label_id: library.label_id,
})
.from(library)
.innerJoin(artists, eq(artists.id, library.artist_id))
Expand All @@ -454,6 +466,7 @@ export const rotation_library_view = wxyc_schema.view('rotation_library_view').a
library_id: library.id,
rotation_id: rotation.id,
label: library.label,
label_id: library.label_id,
play_freq: rotation.play_freq,
album_title: library.album_title,
artist_name: artists.artist_name,
Expand Down
1 change: 1 addition & 0 deletions shared/database/src/types/flowsheet.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface TrackEntryV2 extends BaseEntry {
album_title: string | null;
track_title: string | null;
record_label: string | null;
label_id: number | null;
request_flag: boolean;
rotation_play_freq: string | null;
// Album metadata from cache
Expand Down
Loading
Loading