diff --git a/apps/expo/features/pack-templates/components/TikTokImportModal.tsx b/apps/expo/features/pack-templates/components/OnlineContentImportModal.tsx similarity index 79% rename from apps/expo/features/pack-templates/components/TikTokImportModal.tsx rename to apps/expo/features/pack-templates/components/OnlineContentImportModal.tsx index 885219ee9a..6dd9d06973 100644 --- a/apps/expo/features/pack-templates/components/TikTokImportModal.tsx +++ b/apps/expo/features/pack-templates/components/OnlineContentImportModal.tsx @@ -14,25 +14,25 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useGenerateTemplateFromTikTok } from '../hooks'; +import { useGenerateTemplateFromOnlineContent } from '../hooks'; interface TikTokImportModalProps { visible: boolean; onClose: () => void; } -export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) { +export function OnlineContentImportModal({ visible, onClose }: TikTokImportModalProps) { const { t } = useTranslation(); const { colors } = useColorScheme(); const router = useRouter(); - const [tiktokUrl, setTiktokUrl] = useState(''); + const [onlineContentUrl, setOnlineContentUrl] = useState(''); - const { mutate: generateTemplate, isPending } = useGenerateTemplateFromTikTok(); + const { mutate: generateTemplate, isPending } = useGenerateTemplateFromOnlineContent(); const handleGenerate = () => { - if (!tiktokUrl.trim()) { + if (!onlineContentUrl.trim()) { Burnt.toast({ - title: t('packTemplates.tiktokUrlRequired'), + title: t('packTemplates.onlineContentUrlRequired'), preset: 'error', }); return; @@ -40,14 +40,14 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) generateTemplate( { - tiktokUrl: tiktokUrl.trim(), + contentUrl: onlineContentUrl.trim(), isAppTemplate: true, }, { onSuccess: (template) => { onClose(); Burnt.toast({ - title: t('packTemplates.tiktokImportSuccess'), + title: t('packTemplates.onlineContentImportSuccess'), preset: 'done', }); router.push({ @@ -56,7 +56,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) }); }, onError: (error) => { - console.error('TikTok import error:', error); + console.error('Online content import error:', error); // Handle duplicate template case - navigate to existing template if (error.code === 'DUPLICATE_TEMPLATE' && error.existingTemplateId) { @@ -73,7 +73,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) } // Handle other errors - const errorMessage = error.message || t('packTemplates.tiktokImportError'); + const errorMessage = error.message || t('packTemplates.onlineContentImportError'); Burnt.toast({ title: t('packTemplates.importFailed'), message: errorMessage, @@ -85,7 +85,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) }; const handleClose = () => { - setTiktokUrl(''); + setOnlineContentUrl(''); onClose(); }; @@ -108,7 +108,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) - {t('packTemplates.importFromTikTok')} + {t('packTemplates.importFromOnlineContent')} @@ -117,19 +117,19 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) - {t('packTemplates.tiktokImportDescription')} + {t('packTemplates.onlineContentImportDescription')} - {/* TikTok URL Input */} + {/* Online Content URL Input */} - {t('packTemplates.tiktokUrl')} + {t('packTemplates.onlineContentUrl')} } {isPending - ? t('packTemplates.generatingFromTikTok') - : t('packTemplates.generateFromTikTok')} + ? t('packTemplates.generatingFromOnlineContent') + : t('packTemplates.generateFromOnlineContent')} diff --git a/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx b/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx index 5f5064e76c..695798cc8f 100644 --- a/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx +++ b/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx @@ -11,7 +11,7 @@ import { useRouter } from 'expo-router'; import React, { useState } from 'react'; import { TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { TikTokImportModal } from './TikTokImportModal'; +import { OnlineContentImportModal } from './OnlineContentImportModal'; type TemplateCreationOptionsProps = object; @@ -23,7 +23,7 @@ export default React.forwardRef( const { isAuthenticated } = useAuth(); const user = useUser(); const isAdmin = user?.role === 'ADMIN'; - const [showTikTokModal, setShowTikTokModal] = useState(false); + const [showOnlineContentModal, setShowOnlineContentModal] = useState(false); const insets = useSafeAreaInsets(); const { run, handleDismiss } = useBottomSheetAction(ref as React.RefObject); @@ -34,9 +34,9 @@ export default React.forwardRef( }); }; - const handleImportFromTikTok = () => { + const handleImportFromOnlineContent = () => { run(() => { - setShowTikTokModal(true); + setShowOnlineContentModal(true); }); }; @@ -83,7 +83,7 @@ export default React.forwardRef( {/* Import from TikTok option (only for admins) */} {isAdmin && isAuthenticated && ( @@ -91,10 +91,10 @@ export default React.forwardRef( - {t('packTemplates.importFromTikTok')} + {t('packTemplates.importFromOnlineContent')} - {t('packTemplates.importFromTikTokDescription')} + {t('packTemplates.importFromOnlineContentDescription')} @@ -103,8 +103,10 @@ export default React.forwardRef( - {/* TikTok Import Modal */} - setShowTikTokModal(false)} /> + setShowOnlineContentModal(false)} + /> ); }, diff --git a/apps/expo/features/pack-templates/hooks/index.ts b/apps/expo/features/pack-templates/hooks/index.ts index 0508c1849f..407d9c796f 100644 --- a/apps/expo/features/pack-templates/hooks/index.ts +++ b/apps/expo/features/pack-templates/hooks/index.ts @@ -4,7 +4,7 @@ export * from './useCreatePackTemplate'; export * from './useCreatePackTemplateItem'; export * from './useDeletePackTemplate'; export * from './useDeletePackTemplateItem'; -export * from './useGenerateTemplateFromTikTok'; +export * from './useGenerateTemplateFromOnlineContent'; export * from './usePackTemplateItem'; export * from './usePackTemplates'; export * from './usePackTemplatesDetails'; diff --git a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromTikTok.ts b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts similarity index 80% rename from apps/expo/features/pack-templates/hooks/useGenerateTemplateFromTikTok.ts rename to apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts index e74adeeb82..04131fc8a6 100644 --- a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromTikTok.ts +++ b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts @@ -4,8 +4,8 @@ import { packTemplateItemsStore } from '../store/packTemplateItems'; import { packTemplatesStore } from '../store/packTemplates'; import type { PackTemplateInStore } from '../types'; -export interface GenerateFromTikTokInput { - tiktokUrl: string; +export interface GenerateFromOnlineContentInput { + contentUrl: string; name?: string; category?: string; isAppTemplate?: boolean; @@ -33,18 +33,18 @@ export interface GeneratedTemplate extends PackTemplateInStore { }>; } -export interface TikTokImportError extends Error { +export interface ImportError extends Error { status?: number; code?: string; existingTemplateId?: string; } -export function useGenerateTemplateFromTikTok() { - return useMutation({ +export function useGenerateTemplateFromOnlineContent() { + return useMutation({ mutationFn: async (input) => { try { const response = await axiosInstance.post( - '/api/pack-templates/generate-from-tiktok', + '/api/pack-templates/generate-from-online-content', input, { timeout: 0 }, ); @@ -73,12 +73,12 @@ export function useGenerateTemplateFromTikTok() { } } - const tikTokError = new Error(message) as TikTokImportError; - tikTokError.status = status; - tikTokError.code = errorCode; - tikTokError.existingTemplateId = existingTemplateId; + const importError = new Error(message) as ImportError; + importError.status = status; + importError.code = errorCode; + importError.existingTemplateId = existingTemplateId; - throw tikTokError; + throw importError; } }, onSuccess: (data) => { diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 4ee33bdf9f..b9bb79520a 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -837,21 +837,21 @@ "markAsAppTemplate": "Mark as Featured Template", "appTemplateFootnote": "Featured templates are shown to all users. Option is only available to admins.", "appTemplate": "Featured", - "importFromTikTok": "Import from TikTok", - "importFromTikTokDescription": "Import gear from a TikTok video or slideshow post", - "tiktokUrl": "TikTok URL", - "tiktokUrlPlaceholder": "https://www.tiktok.com/@user/video/...", - "generateFromTikTok": "Generate Template", - "generatingFromTikTok": "Generating...", - "tiktokUrlRequired": "TikTok URL is required", - "tiktokImportSuccess": "Pack template created successfully!", - "tiktokImportError": "Failed to generate template from TikTok. Please try again.", + "importFromOnlineContent": "Import from Online Content", + "importFromOnlineContentDescription": "Supports YouTube and TikTok videos or slideshow posts", + "onlineContentUrl": "Content URL", + "onlineContentUrlPlaceholder": "https://www.tiktok.com/@user/video/...", + "generateFromOnlineContent": "Generate Template", + "generatingFromOnlineContent": "Generating...", + "onlineContentUrlRequired": "Content URL is required", + "onlineContentImportSuccess": "Pack template created successfully!", + "onlineContentImportError": "Failed to generate template.", "templateAlreadyExists": "Template Already Exists", "importFailed": "Import Failed", - "tiktokImportDuplicateError": "A template already exists for this content.", - "tiktokImportServiceError": "TikTok service is unavailable. Please try again later.", - "tiktokImportAIError": "AI analysis failed. Please try again or contact support.", - "tiktokImportDescription": "Paste a TikTok video or slideshow URL below. AI will identify items and build a pack template using our catalog.", + "onlineContentImportDuplicateError": "A template already exists for this content.", + "onlineContentImportServiceError": "Service unavailable.", + "onlineContentImportAIError": "AI analysis failed. Please try again or contact support.", + "onlineContentImportDescription": "Paste a Youtube, TikTok video or slideshow URL below. AI will identify items and build a pack template using our catalog.", "viewExistingTemplate": "View", "creating": "Creating...", "updating": "Updating...", diff --git a/bun.lock b/bun.lock index 352dd3fa20..a0839cc594 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ }, "apps/expo": { "name": "packrat-expo-app", - "version": "2.0.15", + "version": "2.0.16", "dependencies": { "@ai-sdk/react": "^2.0.11", "@expo/react-native-action-sheet": "^4.1.1", @@ -123,7 +123,7 @@ }, "apps/guides": { "name": "packrat-guides-app", - "version": "2.0.15", + "version": "2.0.16", "dependencies": { "@ai-sdk/openai": "^2.0.11", "@hookform/resolvers": "^3.10.0", @@ -202,7 +202,7 @@ }, "apps/landing": { "name": "packrat-landing-app", - "version": "2.0.15", + "version": "2.0.16", "dependencies": { "@emotion/is-prop-valid": "^1.3.1", "@hookform/resolvers": "^3.10.0", @@ -298,6 +298,7 @@ "resend": "^4.2.0", "workers-ai-provider": "^0.7.2", "ws": "^8.18.1", + "youtube-transcript": "^1.3.0", "zod": "^3.24.2", "zod-openapi": "^4.2.4", }, @@ -316,7 +317,7 @@ }, "packages/ui": { "name": "@packrat/ui", - "version": "2.0.15", + "version": "2.0.16", "dependencies": { "@packrat-ai/nativewindui": "^2.0.0", }, @@ -3606,6 +3607,8 @@ "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + "youtube-transcript": ["youtube-transcript@1.3.0", "", {}, "sha512-laWv9RcKIWh6rZUH3hVnOngEvtKAhFMV5UepUO6AgevPYqe2zv8KW/uCkZJDSnPwf5/AdVu0Q66/1RDblKsp6Q=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="], diff --git a/packages/api/package.json b/packages/api/package.json index b60438bc73..aa6138520d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -44,6 +44,7 @@ "resend": "^4.2.0", "workers-ai-provider": "^0.7.2", "ws": "^8.18.1", + "youtube-transcript": "^1.3.0", "zod": "^3.24.2", "zod-openapi": "^4.2.4" }, diff --git a/packages/api/src/routes/packTemplates/generateFromTikTok.ts b/packages/api/src/routes/packTemplates/generateFromOnlineContent.ts similarity index 77% rename from packages/api/src/routes/packTemplates/generateFromTikTok.ts rename to packages/api/src/routes/packTemplates/generateFromOnlineContent.ts index 5abbfc356b..8aa1d29f74 100644 --- a/packages/api/src/routes/packTemplates/generateFromTikTok.ts +++ b/packages/api/src/routes/packTemplates/generateFromOnlineContent.ts @@ -5,8 +5,8 @@ import { createDb } from '@packrat/api/db'; import { packTemplateItems, packTemplates } from '@packrat/api/db/schema'; import { ErrorResponseSchema, - GenerateFromTikTokRequestSchema, - GenerateFromTikTokResponseSchema, + GenerateFromOnlineContentRequestSchema, + GenerateFromOnlineContentResponseSchema, } from '@packrat/api/schemas/packTemplates'; import { CatalogService } from '@packrat/api/services/catalogService'; import type { Env } from '@packrat/api/types/env'; @@ -16,11 +16,12 @@ import { generateObject } from 'ai'; import { sql } from 'drizzle-orm'; import type { Context } from 'hono'; import { nanoid } from 'nanoid'; +import { fetchTranscript } from 'youtube-transcript'; import { z } from 'zod'; /** - * Generate a deterministic content ID from a TikTok URL for duplicate detection - * when the TikTok service doesn't provide a content ID + * Generate a deterministic content ID from a URL for duplicate detection + * when the TikTok or YouTube service doesn't provide a content ID */ function generateContentIdFromUrl(url: string): string { // Normalize the URL by removing query parameters and converting to lowercase @@ -37,9 +38,9 @@ function generateContentIdFromUrl(url: string): string { return `url_${Math.abs(hash).toString(16)}`; } -const SYSTEM_PROMPT = `You are an expert outdoor gear analyst. You will be shown content from TikTok featuring packing content (e.g., a gear lay-flat, kit breakdown, or packing list). This content may be either images (slideshow) or a video. Your task is to: +const SYSTEM_PROMPT = `You are an expert outdoor gear analyst. You will be shown content from TikTok or YouTube featuring packing content (e.g., a gear lay-flat, kit breakdown, or packing list). This content may be either images (slideshow), a video or video transcript. Your task is to: -1. Identify every outdoor gear or equipment item visible in the images/video or mentioned in the caption. +1. Identify every outdoor gear or equipment item visible in the images/video or mentioned in the caption/transcript. 2. For each item, provide a specific name, description, category, weight estimate (in grams), quantity, and flags for whether it is consumable or worn. 3. Also determine an appropriate pack template name and category (one of: hiking, backpacking, camping, climbing, winter, desert, custom, water sports, skiing) for this overall kit. @@ -103,6 +104,44 @@ async function fetchTikTokPostData( } } +/** + * Check if a URL is a YouTube URL + */ +function isYouTubeUrl(url: string): boolean { + try { + const parsed = new URL(url); + const hostname = parsed.hostname.replace('www.', ''); + + return hostname === 'youtube.com' || hostname === 'youtu.be'; + } catch { + return false; + } +} + +/** + * Extract the YouTube video ID from a URL + * @param url The YouTube URL + * @returns The video ID or null if not found + */ +function getYouTubeId(url: string): string | null { + try { + const parsed = new URL(url); + const host = parsed.hostname.replace('www.', ''); + + if (host === 'youtu.be') { + return parsed.pathname.slice(1); + } + + if (host === 'youtube.com') { + return parsed.searchParams.get('v'); + } + + return null; + } catch { + return null; + } +} + const analysisSchema = z.object({ templateName: z.string().describe('A name for this pack template'), templateCategory: z @@ -138,20 +177,19 @@ const analysisSchema = z.object({ .describe('All gear items identified from the content'), }); -// POST /pack-templates/generate-from-tiktok -const generateFromTikTokRoute = createRoute({ +const generateFromOnlineContentRoute = createRoute({ method: 'post', - path: '/generate-from-tiktok', + path: '/generate-from-online-content', tags: ['Pack Templates'], - summary: 'Generate a pack template from a TikTok content URL', + summary: 'Generate a pack template from an online content URL', description: - 'Admin-only endpoint that uses TikTok API to fetch slideshow images or videos and captions from a TikTok URL, then analyzes the content with AI (Gemini-3-Flash-Preview) to build a featured pack template using items from the catalog.', + 'Admin-only endpoint that retrieves TikTok slideshow images, videos and captions or YouTube video transcripts, then analyzes the content with AI (Gemini-3-Flash-Preview) to build a featured pack template using items from the catalog.', security: [{ bearerAuth: [] }], request: { body: { content: { 'application/json': { - schema: GenerateFromTikTokRequestSchema, + schema: GenerateFromOnlineContentRequestSchema, }, }, required: true, @@ -162,7 +200,7 @@ const generateFromTikTokRoute = createRoute({ description: 'Pack template generated and created successfully', content: { 'application/json': { - schema: GenerateFromTikTokResponseSchema, + schema: GenerateFromOnlineContentResponseSchema, }, }, }, @@ -183,7 +221,7 @@ const generateFromTikTokRoute = createRoute({ }, }, 409: { - description: 'Conflict - Template already exists for this TikTok content.', + description: 'Conflict - Template already exists for this content.', content: { 'application/json': { schema: ErrorResponseSchema, @@ -201,13 +239,13 @@ const generateFromTikTokRoute = createRoute({ }, }); -const generateFromTikTokRoutes = new OpenAPIHono<{ +const generateFromOnlineContentRoutes = new OpenAPIHono<{ Bindings: Env; Variables: Variables; }>(); -generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { - let tiktokUrl: string | undefined; +generateFromOnlineContentRoutes.openapi(generateFromOnlineContentRoute, async (c) => { + let contentUrl: string | undefined; try { const auth = c.get('user'); @@ -217,31 +255,48 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { const body = c.req.valid('json'); const { isAppTemplate } = body; - tiktokUrl = body.tiktokUrl; + contentUrl = body.contentUrl; const { GOOGLE_GENERATIVE_AI_API_KEY } = getEnv(c); const google = createGoogleGenerativeAI({ apiKey: GOOGLE_GENERATIVE_AI_API_KEY, }); - // Fetch TikTok data using API library - console.log(`Processing TikTok URL: ${tiktokUrl}`); + console.log(`Processing content: ${contentUrl}`); let imageUrls: string[]; let videoUrl: string | undefined; let caption: string | undefined; + let youtubeVideoTranscript: string | undefined; let contentId: string | undefined; + let contentSource: string | undefined; try { - const data = await fetchTikTokPostData(c, tiktokUrl); - imageUrls = data.imageUrls; - videoUrl = data.videoUrl; - caption = data.caption; - contentId = data.contentId; + if (isYouTubeUrl(contentUrl)) { + const youtubeId = getYouTubeId(contentUrl); + if (!youtubeId) { + throw new Error('Invalid YouTube URL'); + } + + contentId = youtubeId; + + youtubeVideoTranscript = (await fetchTranscript(youtubeId)) + .reduce((acc, curr) => `${acc} ${curr.text}`, '') + .trim(); + + contentSource = 'youtube'; + } else { + const data = await fetchTikTokPostData(c, contentUrl); + imageUrls = data.imageUrls; + videoUrl = data.videoUrl; + caption = data.caption; + contentId = data.contentId; + contentSource = 'tiktok'; + } } catch (apiError) { console.error('TikTok service call failed:', apiError); c.get('sentry').captureException(apiError, { - extra: { tiktokUrl, errorType: 'tiktok_service_error' }, + extra: { tiktokUrl: contentUrl, errorType: 'tiktok_service_error' }, } as unknown); return c.json( { @@ -254,7 +309,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { // Ensure we have a contentId for reliable duplicate detection // Use TikTok service contentId if available, otherwise generate from URL - const finalContentId = contentId || generateContentIdFromUrl(tiktokUrl); + const finalContentId = contentId || generateContentIdFromUrl(contentUrl); // Check for existing template with same content ID to avoid duplication const db = createDb(c); @@ -262,7 +317,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { .select() .from(packTemplates) .where( - sql`${packTemplates.contentSource} = 'tiktok' AND ${packTemplates.contentId} = ${finalContentId} AND ${packTemplates.deleted} = false`, + sql`${packTemplates.contentSource} = ${contentSource} AND ${packTemplates.contentId} = ${finalContentId} AND ${packTemplates.deleted} = false`, ) .limit(1); @@ -270,7 +325,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { const existing = existingTemplate[0]!; return c.json( { - error: 'Template already exists for this TikTok content.', + error: 'Template already exists for this content.', code: 'DUPLICATE_TEMPLATE', existingTemplateId: existing.id, }, @@ -285,7 +340,12 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { const contentParts: Array = []; let introText: string; - if (videoUrl) { + if (youtubeVideoTranscript) { + introText = + 'Please analyze the YouTube video transcript below and identify all packing/gear items:'; + contentParts.push({ type: 'text', text: introText }); + contentParts.push({ type: 'text', text: youtubeVideoTranscript }); + } else if (videoUrl) { introText = caption ? `Retrieved Caption: ${caption}\n\nPlease analyze the following TikTok video and identify all packing/gear items:` : `Please analyze the following TikTok video and identify all packing/gear items:`; @@ -345,7 +405,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { tags: [analysis.templateCategory], isAppTemplate: isAppTemplate ?? true, deleted: false, - contentSource: 'tiktok', + contentSource, contentId: finalContentId, localCreatedAt: now, localUpdatedAt: now, @@ -387,9 +447,9 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { return c.json({ ...newTemplate, items: insertedItems }, 201); } catch (error) { - console.error('Error generating pack template from TikTok:', error); + console.error('Error generating pack template:', error); c.get('sentry').captureException(error, { - extra: { tiktokUrl, errorType: 'template_generation_error' }, + extra: { contentUrl, errorType: 'template_generation_error' }, } as any); // Determine specific error type based on error context @@ -418,4 +478,4 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { } }); -export { generateFromTikTokRoutes }; +export { generateFromOnlineContentRoutes }; diff --git a/packages/api/src/routes/packTemplates/index.ts b/packages/api/src/routes/packTemplates/index.ts index e51c7e4321..d4b896c977 100644 --- a/packages/api/src/routes/packTemplates/index.ts +++ b/packages/api/src/routes/packTemplates/index.ts @@ -1,5 +1,5 @@ import { OpenAPIHono } from '@hono/zod-openapi'; -import { generateFromTikTokRoutes } from './generateFromTikTok'; +import { generateFromOnlineContentRoutes } from './generateFromOnlineContent'; import { packTemplateItemsRoutes } from './packTemplateItems'; import { packTemplateRoutes } from './packTemplates'; @@ -7,6 +7,6 @@ const packTemplatesRoutes = new OpenAPIHono(); packTemplatesRoutes.route('/', packTemplateRoutes); packTemplatesRoutes.route('/', packTemplateItemsRoutes); -packTemplatesRoutes.route('/', generateFromTikTokRoutes); +packTemplatesRoutes.route('/', generateFromOnlineContentRoutes); export { packTemplatesRoutes }; diff --git a/packages/api/src/schemas/packTemplates.ts b/packages/api/src/schemas/packTemplates.ts index cb2dd46b45..6da5a0c591 100644 --- a/packages/api/src/schemas/packTemplates.ts +++ b/packages/api/src/schemas/packTemplates.ts @@ -311,17 +311,17 @@ export const SuccessResponseSchema = z }) .openapi('SuccessResponse'); -export const GenerateFromTikTokRequestSchema = z +export const GenerateFromOnlineContentRequestSchema = z .object({ - tiktokUrl: z.string().url().openapi({ + contentUrl: z.string().url().openapi({ example: 'https://www.tiktok.com/@user/video/1234567890', - description: 'The TikTok content URL (supports both slideshows and videos)', + description: 'The content URL (supports YouTube and TikTok content)', }), isAppTemplate: z.boolean().optional().default(true).openapi({ example: true, description: 'Whether this should be a featured template (admin only)', }), }) - .openapi('GenerateFromTikTokRequest'); + .openapi('GenerateFromOnlineContentRequest'); -export const GenerateFromTikTokResponseSchema = PackTemplateWithItemsSchema; +export const GenerateFromOnlineContentResponseSchema = PackTemplateWithItemsSchema;