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;