feat(S02/T02): Wired monitoringMode through channel creation (route sch…
- src/server/routes/channel.ts - src/frontend/src/api/hooks/useChannels.ts - src/frontend/src/components/AddChannelModal.tsx - src/__tests__/channel.test.ts
This commit is contained in:
parent
6715c9b6fe
commit
0ef34b1d21
4 changed files with 150 additions and 3 deletions
|
|
@ -260,6 +260,92 @@ describe('Channel API', () => {
|
|||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('creates channel with monitoringMode=future and monitoringEnabled=true', async () => {
|
||||
execYtDlpMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
channel: 'Future Only Channel',
|
||||
channel_id: 'UC_FUTURE_001',
|
||||
channel_url: 'https://www.youtube.com/@FutureOnly',
|
||||
uploader: 'Future Only Channel',
|
||||
thumbnails: [{ url: 'https://yt.com/future_thumb.jpg' }],
|
||||
}),
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
});
|
||||
|
||||
const res = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/channel',
|
||||
headers: { 'x-api-key': apiKey },
|
||||
payload: {
|
||||
url: 'https://www.youtube.com/@FutureOnly',
|
||||
monitoringMode: 'future',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
expect(body.monitoringMode).toBe('future');
|
||||
expect(body.monitoringEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('creates channel with monitoringMode=none and monitoringEnabled=false', async () => {
|
||||
execYtDlpMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
channel: 'No Monitor Channel',
|
||||
channel_id: 'UC_NOMONITOR_001',
|
||||
channel_url: 'https://www.youtube.com/@NoMonitor',
|
||||
uploader: 'No Monitor Channel',
|
||||
thumbnails: [{ url: 'https://yt.com/nomonitor_thumb.jpg' }],
|
||||
}),
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
});
|
||||
|
||||
const res = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/channel',
|
||||
headers: { 'x-api-key': apiKey },
|
||||
payload: {
|
||||
url: 'https://www.youtube.com/@NoMonitor',
|
||||
monitoringMode: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
expect(body.monitoringMode).toBe('none');
|
||||
expect(body.monitoringEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults monitoringMode to all when not specified', async () => {
|
||||
execYtDlpMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
channel: 'Default Mode Channel',
|
||||
channel_id: 'UC_DEFAULTMODE_001',
|
||||
channel_url: 'https://www.youtube.com/@DefaultMode',
|
||||
uploader: 'Default Mode Channel',
|
||||
thumbnails: [{ url: 'https://yt.com/defaultmode_thumb.jpg' }],
|
||||
}),
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
});
|
||||
|
||||
const res = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/channel',
|
||||
headers: { 'x-api-key': apiKey },
|
||||
payload: {
|
||||
url: 'https://www.youtube.com/@DefaultMode',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
expect(body.monitoringMode).toBe('all');
|
||||
expect(body.monitoringEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── GET /api/v1/channel ──
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ interface CreateChannelInput {
|
|||
url: string;
|
||||
checkInterval?: number;
|
||||
monitoringEnabled?: boolean;
|
||||
monitoringMode?: string;
|
||||
formatProfileId?: number;
|
||||
grabAll?: boolean;
|
||||
grabAllOrder?: 'newest' | 'oldest';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Modal } from './Modal';
|
|||
import { useCreateChannel } from '../api/hooks/useChannels';
|
||||
import { usePlatformSettings } from '../api/hooks/usePlatformSettings';
|
||||
import { useFormatProfiles } from '../api/hooks/useFormatProfiles';
|
||||
import { apiClient } from '../api/client';
|
||||
import { Loader, CheckCircle } from 'lucide-react';
|
||||
import type { Platform } from '@shared/types/index';
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) {
|
|||
const [url, setUrl] = useState('');
|
||||
const [checkInterval, setCheckInterval] = useState('');
|
||||
const [formatProfileId, setFormatProfileId] = useState<number | undefined>(undefined);
|
||||
const [monitoringMode, setMonitoringMode] = useState<string>('all');
|
||||
const [grabAll, setGrabAll] = useState(false);
|
||||
const [grabAllOrder, setGrabAllOrder] = useState<'newest' | 'oldest'>('newest');
|
||||
|
||||
|
|
@ -76,6 +78,11 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) {
|
|||
setFormatProfileId(settings.defaultFormatProfileId);
|
||||
}
|
||||
|
||||
// Pre-fill monitoring mode from platform defaults
|
||||
if (settings.defaultMonitoringMode) {
|
||||
setMonitoringMode(settings.defaultMonitoringMode);
|
||||
}
|
||||
|
||||
// Pre-fill grab-all defaults for YouTube
|
||||
if (detectedPlatform === 'youtube') {
|
||||
if (settings.grabAllEnabled) {
|
||||
|
|
@ -95,14 +102,22 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) {
|
|||
{
|
||||
url: url.trim(),
|
||||
checkInterval: checkInterval ? parseInt(checkInterval, 10) : undefined,
|
||||
monitoringMode,
|
||||
formatProfileId: formatProfileId ?? undefined,
|
||||
grabAll: detectedPlatform === 'youtube' ? grabAll : undefined,
|
||||
grabAllOrder: detectedPlatform === 'youtube' && grabAll ? grabAllOrder : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onSuccess: (newChannel) => {
|
||||
resetForm();
|
||||
onClose();
|
||||
// Fire-and-forget initial scan so content appears immediately
|
||||
apiClient
|
||||
.post(`/api/v1/channel/${newChannel.id}/scan`)
|
||||
.catch(() => {
|
||||
// Auto-scan failure is non-critical — channel was created successfully
|
||||
console.warn(`[AddChannel] Auto-scan failed for channel ${newChannel.id}`);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -112,6 +127,7 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) {
|
|||
setUrl('');
|
||||
setCheckInterval('');
|
||||
setFormatProfileId(undefined);
|
||||
setMonitoringMode('all');
|
||||
setGrabAll(false);
|
||||
setGrabAllOrder('newest');
|
||||
createChannel.reset();
|
||||
|
|
@ -232,6 +248,35 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Monitoring Mode — shown when platform detected */}
|
||||
{detectedPlatform && (
|
||||
<div style={{ marginBottom: 'var(--space-4)' }}>
|
||||
<label
|
||||
htmlFor="monitoring-mode"
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 'var(--space-1)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
fontWeight: 500,
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
Monitoring Mode
|
||||
</label>
|
||||
<select
|
||||
id="monitoring-mode"
|
||||
value={monitoringMode}
|
||||
onChange={(e) => setMonitoringMode(e.target.value)}
|
||||
disabled={createChannel.isPending}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<option value="all">Monitor All</option>
|
||||
<option value="future">Future Only</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grab All — YouTube only */}
|
||||
{detectedPlatform === 'youtube' && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const createChannelBodySchema = {
|
|||
url: { type: 'string' as const },
|
||||
checkInterval: { type: 'number' as const, minimum: 1 },
|
||||
monitoringEnabled: { type: 'boolean' as const },
|
||||
monitoringMode: { type: 'string' as const, enum: ['all', 'future', 'existing', 'none'] },
|
||||
formatProfileId: { type: 'number' as const },
|
||||
grabAll: { type: 'boolean' as const },
|
||||
grabAllOrder: { type: 'string' as const, enum: ['newest', 'oldest'] },
|
||||
|
|
@ -89,6 +90,7 @@ export async function channelRoutes(fastify: FastifyInstance): Promise<void> {
|
|||
url: string;
|
||||
checkInterval?: number;
|
||||
monitoringEnabled?: boolean;
|
||||
monitoringMode?: MonitoringMode;
|
||||
formatProfileId?: number;
|
||||
grabAll?: boolean;
|
||||
grabAllOrder?: 'newest' | 'oldest';
|
||||
|
|
@ -99,7 +101,7 @@ export async function channelRoutes(fastify: FastifyInstance): Promise<void> {
|
|||
schema: { body: createChannelBodySchema },
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { url, checkInterval, monitoringEnabled, formatProfileId, grabAll, grabAllOrder } = request.body;
|
||||
const { url, checkInterval, monitoringEnabled, monitoringMode, formatProfileId, grabAll, grabAllOrder } = request.body;
|
||||
|
||||
// Validate URL against registered platforms
|
||||
const match = registry.getForUrl(url);
|
||||
|
|
@ -150,13 +152,26 @@ export async function channelRoutes(fastify: FastifyInstance): Promise<void> {
|
|||
10
|
||||
);
|
||||
|
||||
// Derive monitoringEnabled from monitoringMode when provided;
|
||||
// preserve backward compat when only monitoringEnabled is sent
|
||||
const resolvedMode = monitoringMode ?? 'all';
|
||||
const resolvedEnabled = monitoringMode
|
||||
? monitoringMode !== 'none'
|
||||
: (monitoringEnabled ?? true);
|
||||
|
||||
request.log.info(
|
||||
{ resolvedMode, resolvedEnabled, platform: metadata.platform },
|
||||
'[channel] Creating channel with monitoring mode'
|
||||
);
|
||||
|
||||
// Insert channel
|
||||
const channel = await createChannel(fastify.db, {
|
||||
name: metadata.name,
|
||||
platform: metadata.platform,
|
||||
platformId: metadata.platformId,
|
||||
url: metadata.url,
|
||||
monitoringEnabled: monitoringEnabled ?? true,
|
||||
monitoringEnabled: resolvedEnabled,
|
||||
monitoringMode: resolvedMode,
|
||||
checkInterval: checkInterval ?? defaultInterval,
|
||||
imageUrl: metadata.imageUrl,
|
||||
metadata: null,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue