From 1eb296cd8bcfa5523778e2876d8e4b3164e02350 Mon Sep 17 00:00:00 2001 From: Alenvelocity Date: Tue, 1 Jul 2025 21:07:15 +0530 Subject: [PATCH] app: clean up and update request types --- api/package.json | 1 - api/src/processing/match.js | 6 + api/src/processing/services/youtube.js | 15 +- api/src/stream/ffmpeg.js | 13 +- pnpm-lock.yaml | 8 - web/src/components/save/ClipControls.svelte | 247 ++++++++++++++++++ .../components/save/ClipRangeSlider.svelte | 2 +- web/src/components/save/Omnibox.svelte | 227 +--------------- .../save/buttons/ClipCheckbox.svelte | 2 +- .../save/buttons/DownloadButton.svelte | 5 +- web/src/lib/types/api.ts | 5 +- 11 files changed, 276 insertions(+), 255 deletions(-) create mode 100644 web/src/components/save/ClipControls.svelte diff --git a/api/package.json b/api/package.json index 51559ff7..1611247d 100644 --- a/api/package.json +++ b/api/package.json @@ -32,7 +32,6 @@ "express": "^4.21.2", "express-rate-limit": "^7.4.1", "ffmpeg-static": "^5.1.0", - "ffprobe-static": "^3.1.0", "hls-parser": "^0.10.7", "ipaddr.js": "2.2.0", "mime": "^4.0.4", diff --git a/api/src/processing/match.js b/api/src/processing/match.js index 35a75384..5dc4f0e0 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -330,11 +330,17 @@ export default async function({ host, patternMatch, params, authType }) { const lpEnv = env.forceLocalProcessing; const shouldForceLocal = lpEnv === "always" || (lpEnv === "session" && authType === "session"); const localDisabled = (!localProcessing || localProcessing === "none"); + const isClip = typeof params.clipStart === 'number' && typeof params.clipEnd === 'number'; if (shouldForceLocal && localDisabled) { localProcessing = "preferred"; } + if (isClip) { + r.clipStart = params.clipStart; + r.clipEnd = params.clipEnd; + } + return matchAction({ r, host, diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index 003cd54d..3149ea56 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -291,13 +291,6 @@ export default async function (o) { return { error: "content.too_long" }; } - if (typeof o.clipStart === 'number' && o.clipStart >= basicInfo.duration) { - return { error: "clip.start_exceeds_duration" }; - } - if (typeof o.clipEnd === 'number' && o.clipEnd > basicInfo.duration) { - return { error: "clip.end_exceeds_duration" }; - } - // return a critical error if returned video is "Video Not Available" // or a similar stub by youtube if (basicInfo.id !== o.id) { @@ -487,7 +480,7 @@ export default async function (o) { const fileMetadata = { title: basicInfo.title.trim(), - artist: basicInfo.author.replace("- Topic", "").trim(), + artist: basicInfo.author.replace("- Topic", "").trim() } if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) { @@ -581,8 +574,6 @@ export default async function (o) { cover, cropCover: basicInfo.author.endsWith("- Topic"), - clipStart: o.clipStart, - clipEnd: o.clipEnd, } } @@ -628,9 +619,7 @@ export default async function (o) { fileMetadata, isHLS: useHLS, originalRequest, - duration: basicInfo.duration, - clipStart: o.clipStart, - clipEnd: o.clipEnd, + duration: basicInfo.duration } } diff --git a/api/src/stream/ffmpeg.js b/api/src/stream/ffmpeg.js index c5c924f1..2899eca0 100644 --- a/api/src/stream/ffmpeg.js +++ b/api/src/stream/ffmpeg.js @@ -100,9 +100,7 @@ const remux = async (streamInfo, res) => { const format = streamInfo.filename.split('.').pop(); const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls]; const isClipping = typeof streamInfo.clipStart === 'number' || typeof streamInfo.clipEnd === 'number'; - let args = []; - - args.push(...urls.flatMap(url => ['-i', url])); + const args = urls.flatMap(url => ['-i', url]); if (typeof streamInfo.clipStart === 'number') { args.push('-ss', streamInfo.clipStart.toString()); @@ -110,7 +108,8 @@ const remux = async (streamInfo, res) => { if (typeof streamInfo.clipEnd === 'number') { args.push('-to', streamInfo.clipEnd.toString()); } - + + // if the stream type is merge, we expect two URLs if (streamInfo.type === 'merge' && urls.length !== 2) { return closeResponse(res); } @@ -171,13 +170,11 @@ const remux = async (streamInfo, res) => { } const convertAudio = async (streamInfo, res) => { - let args = []; - - args.push( + const args = [ '-i', streamInfo.urls, '-vn', ...(streamInfo.audioCopy ? ['-c:a', 'copy'] : ['-b:a', `${streamInfo.audioBitrate}k`]), - ); + ]; if (typeof streamInfo.clipStart === 'number') { args.push('-ss', streamInfo.clipStart.toString()); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35d7163d..6d67cc68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,9 +37,6 @@ importers: ffmpeg-static: specifier: ^5.1.0 version: 5.2.0 - ffprobe-static: - specifier: ^3.1.0 - version: 3.1.0 hls-parser: specifier: ^0.10.7 version: 0.10.9 @@ -1257,9 +1254,6 @@ packages: resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==} engines: {node: '>=16'} - ffprobe-static@3.1.0: - resolution: {integrity: sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3195,8 +3189,6 @@ snapshots: transitivePeerDependencies: - supports-color - ffprobe-static@3.1.0: {} - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 diff --git a/web/src/components/save/ClipControls.svelte b/web/src/components/save/ClipControls.svelte new file mode 100644 index 00000000..d2ffec8b --- /dev/null +++ b/web/src/components/save/ClipControls.svelte @@ -0,0 +1,247 @@ + + +
+ {#if metaLoading} +
+
+ Loading video metadata... +
+ {:else if metaError} +
+ + + + + + {metaError} +
+ {:else if metadata} + + + + +
+
+ {formatTime(clipStart)} + + {formatTime(Math.max(0, clipEnd - clipStart))} selected + + {formatTime(clipEnd)} +
+
+ {/if} +
+ + diff --git a/web/src/components/save/ClipRangeSlider.svelte b/web/src/components/save/ClipRangeSlider.svelte index fdf7271a..e272a2f0 100644 --- a/web/src/components/save/ClipRangeSlider.svelte +++ b/web/src/components/save/ClipRangeSlider.svelte @@ -345,4 +345,4 @@ transition: none; } } - \ No newline at end of file + diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index 484245e2..d1fefce1 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -37,6 +37,7 @@ import API from "$lib/api/api"; import ClipRangeSlider from "./ClipRangeSlider.svelte"; import ClipCheckbox from "./buttons/ClipCheckbox.svelte"; + import ClipControls from "./ClipControls.svelte"; let linkInput: Optional; @@ -284,60 +285,13 @@ {#if clipMode && validLink($link)} -
- {#if metaLoading} -
-
- Loading video metadata... -
- {:else if metaError} -
- - - - - - {metaError} -
- {:else if metadata} - - - - -
-
- {formatTime(clipStart)} - - {formatTime(Math.max(0, clipEnd - clipStart))} selected - - {formatTime(clipEnd)} -
-
- {/if} -
+ {/if} @@ -467,140 +421,6 @@ font-weight: 500; } - .clip-controls { - margin-top: 12px; - padding: 16px; - background: var(--button); - border-radius: var(--border-radius); - border: 1px solid var(--button-stroke); - animation: slideIn 0.3s cubic-bezier(0.2, 0, 0, 1); - } - - .loading-state { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 0; - color: var(--gray); - font-size: 14px; - } - - .loading-spinner { - width: 16px; - height: 16px; - border: 2px solid var(--button-hover); - border-top: 2px solid var(--secondary); - border-radius: 50%; - animation: spin 1s linear infinite; - } - - .error-state { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - background: rgba(237, 34, 54, 0.1); - border: 1px solid rgba(237, 34, 54, 0.2); - border-radius: 8px; - color: var(--red); - font-size: 13px; - font-weight: 500; - } - - .clip-metadata { - margin-bottom: 16px; - } - - .metadata-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; - padding-bottom: 8px; - } - - .metadata-header h4 { - margin: 0; - font-size: 15px; - font-weight: 600; - color: var(--secondary); - } - - .duration-badge { - background: var(--button-elevated); - color: var(--button-text); - padding: 4px 8px; - border-radius: 6px; - font-size: 11px; - font-weight: 600; - font-family: 'IBM Plex Mono', monospace; - } - - .video-info { - display: flex; - flex-direction: column; - gap: 4px; - } - - .video-title { - font-size: 14px; - font-weight: 500; - color: var(--secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; - } - - .video-author { - font-size: 12px; - color: var(--gray); - font-weight: 400; - } - - .clip-time-display { - margin-top: 8px; - } - - .time-indicators { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - font-weight: 500; - font-family: 'IBM Plex Mono', monospace; - } - - .start-time, .end-time { - color: var(--gray); - background: var(--button-hover); - padding: 2px 6px; - border-radius: 4px; - min-width: 50px; - text-align: center; - } - - .duration-selected { - grid-area: duration; - text-align: center; - } - - @keyframes slideIn { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - @media screen and (max-width: 440px) { #action-container { flex-direction: column; @@ -618,36 +438,5 @@ #paste-desktop-text { display: none; } - - .clip-controls { - padding: 12px; - } - - .metadata-header { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .video-title { - white-space: normal; - } - - .time-indicators { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-areas: - "start end" - "duration duration"; - gap: 8px; - } - - .start-time { - grid-area: start; - } - - .end-time { - grid-area: end; - } } diff --git a/web/src/components/save/buttons/ClipCheckbox.svelte b/web/src/components/save/buttons/ClipCheckbox.svelte index 22f36aa8..2a88fd44 100644 --- a/web/src/components/save/buttons/ClipCheckbox.svelte +++ b/web/src/components/save/buttons/ClipCheckbox.svelte @@ -97,4 +97,4 @@ font-size: 14.5px; } } - \ No newline at end of file + diff --git a/web/src/components/save/buttons/DownloadButton.svelte b/web/src/components/save/buttons/DownloadButton.svelte index a7662ee1..532533dc 100644 --- a/web/src/components/save/buttons/DownloadButton.svelte +++ b/web/src/components/save/buttons/DownloadButton.svelte @@ -61,12 +61,11 @@ on:click={() => { hapticSwitch(); if (clipMode) { - const req: CobaltSaveRequestBody & { clipStart?: number; clipEnd?: number } = { + savingHandler({ request: { url, clipStart, clipEnd, - }; - savingHandler({ request: req }); + } }); } else { savingHandler({ url }); } diff --git a/web/src/lib/types/api.ts b/web/src/lib/types/api.ts index f127d02e..58a5c7e5 100644 --- a/web/src/lib/types/api.ts +++ b/web/src/lib/types/api.ts @@ -113,7 +113,10 @@ export type CobaltServerInfo = { // this allows for extra properties, which is not ideal, // but i couldn't figure out how to make a strict partial :( export type CobaltSaveRequestBody = - { url: string } & Partial>; + { url: string } & Partial> & { + clipStart?: number, + clipEnd?: number, + }; export type CobaltSessionResponse = CobaltSession | CobaltErrorResponse; export type CobaltServerInfoResponse = CobaltServerInfo | CobaltErrorResponse;