app: clean up and update request types

This commit is contained in:
Alenvelocity 2025-07-01 21:07:15 +05:30
parent 2a1aaef0b0
commit 1eb296cd8b
11 changed files with 276 additions and 255 deletions

View File

@ -32,7 +32,6 @@
"express": "^4.21.2", "express": "^4.21.2",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"ffmpeg-static": "^5.1.0", "ffmpeg-static": "^5.1.0",
"ffprobe-static": "^3.1.0",
"hls-parser": "^0.10.7", "hls-parser": "^0.10.7",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"mime": "^4.0.4", "mime": "^4.0.4",

View File

@ -330,11 +330,17 @@ export default async function({ host, patternMatch, params, authType }) {
const lpEnv = env.forceLocalProcessing; const lpEnv = env.forceLocalProcessing;
const shouldForceLocal = lpEnv === "always" || (lpEnv === "session" && authType === "session"); const shouldForceLocal = lpEnv === "always" || (lpEnv === "session" && authType === "session");
const localDisabled = (!localProcessing || localProcessing === "none"); const localDisabled = (!localProcessing || localProcessing === "none");
const isClip = typeof params.clipStart === 'number' && typeof params.clipEnd === 'number';
if (shouldForceLocal && localDisabled) { if (shouldForceLocal && localDisabled) {
localProcessing = "preferred"; localProcessing = "preferred";
} }
if (isClip) {
r.clipStart = params.clipStart;
r.clipEnd = params.clipEnd;
}
return matchAction({ return matchAction({
r, r,
host, host,

View File

@ -291,13 +291,6 @@ export default async function (o) {
return { error: "content.too_long" }; 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" // return a critical error if returned video is "Video Not Available"
// or a similar stub by youtube // or a similar stub by youtube
if (basicInfo.id !== o.id) { if (basicInfo.id !== o.id) {
@ -487,7 +480,7 @@ export default async function (o) {
const fileMetadata = { const fileMetadata = {
title: basicInfo.title.trim(), 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")) { if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) {
@ -581,8 +574,6 @@ export default async function (o) {
cover, cover,
cropCover: basicInfo.author.endsWith("- Topic"), cropCover: basicInfo.author.endsWith("- Topic"),
clipStart: o.clipStart,
clipEnd: o.clipEnd,
} }
} }
@ -628,9 +619,7 @@ export default async function (o) {
fileMetadata, fileMetadata,
isHLS: useHLS, isHLS: useHLS,
originalRequest, originalRequest,
duration: basicInfo.duration, duration: basicInfo.duration
clipStart: o.clipStart,
clipEnd: o.clipEnd,
} }
} }

View File

@ -100,9 +100,7 @@ const remux = async (streamInfo, res) => {
const format = streamInfo.filename.split('.').pop(); const format = streamInfo.filename.split('.').pop();
const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls]; const urls = Array.isArray(streamInfo.urls) ? streamInfo.urls : [streamInfo.urls];
const isClipping = typeof streamInfo.clipStart === 'number' || typeof streamInfo.clipEnd === 'number'; const isClipping = typeof streamInfo.clipStart === 'number' || typeof streamInfo.clipEnd === 'number';
let args = []; const args = urls.flatMap(url => ['-i', url]);
args.push(...urls.flatMap(url => ['-i', url]));
if (typeof streamInfo.clipStart === 'number') { if (typeof streamInfo.clipStart === 'number') {
args.push('-ss', streamInfo.clipStart.toString()); args.push('-ss', streamInfo.clipStart.toString());
@ -111,6 +109,7 @@ const remux = async (streamInfo, res) => {
args.push('-to', streamInfo.clipEnd.toString()); args.push('-to', streamInfo.clipEnd.toString());
} }
// if the stream type is merge, we expect two URLs
if (streamInfo.type === 'merge' && urls.length !== 2) { if (streamInfo.type === 'merge' && urls.length !== 2) {
return closeResponse(res); return closeResponse(res);
} }
@ -171,13 +170,11 @@ const remux = async (streamInfo, res) => {
} }
const convertAudio = async (streamInfo, res) => { const convertAudio = async (streamInfo, res) => {
let args = []; const args = [
args.push(
'-i', streamInfo.urls, '-i', streamInfo.urls,
'-vn', '-vn',
...(streamInfo.audioCopy ? ['-c:a', 'copy'] : ['-b:a', `${streamInfo.audioBitrate}k`]), ...(streamInfo.audioCopy ? ['-c:a', 'copy'] : ['-b:a', `${streamInfo.audioBitrate}k`]),
); ];
if (typeof streamInfo.clipStart === 'number') { if (typeof streamInfo.clipStart === 'number') {
args.push('-ss', streamInfo.clipStart.toString()); args.push('-ss', streamInfo.clipStart.toString());

View File

@ -37,9 +37,6 @@ importers:
ffmpeg-static: ffmpeg-static:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.2.0 version: 5.2.0
ffprobe-static:
specifier: ^3.1.0
version: 3.1.0
hls-parser: hls-parser:
specifier: ^0.10.7 specifier: ^0.10.7
version: 0.10.9 version: 0.10.9
@ -1257,9 +1254,6 @@ packages:
resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==} resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==}
engines: {node: '>=16'} engines: {node: '>=16'}
ffprobe-static@3.1.0:
resolution: {integrity: sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==}
file-entry-cache@8.0.0: file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -3195,8 +3189,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
ffprobe-static@3.1.0: {}
file-entry-cache@8.0.0: file-entry-cache@8.0.0:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1

View File

@ -0,0 +1,247 @@
<script lang="ts">
import ClipRangeSlider from "./ClipRangeSlider.svelte";
let {
metaLoading,
metaError,
metadata,
clipStart = $bindable(),
clipEnd = $bindable()
} = $props<{
metaLoading: boolean;
metaError: string | null;
metadata: { title?: string; author?: string; duration?: number } | null;
clipStart: number;
clipEnd: number;
}>();
function formatTime(seconds: number) {
const m = Math.floor(seconds / 60);
const s = (seconds % 60).toFixed(3).padStart(6, '0');
return `${m}:${s}`;
}
</script>
<div class="clip-controls">
{#if metaLoading}
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Loading video metadata...</span>
</div>
{:else if metaError}
<div class="error-state">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<span>{metaError}</span>
</div>
{:else if metadata}
<div class="clip-metadata">
<div class="metadata-header">
<h4>Clip</h4>
<div class="duration-badge">
{typeof metadata.duration === 'number' ? formatTime(metadata.duration) : 'Unknown'}
</div>
</div>
<div class="video-info">
<div class="video-title" title={metadata.title}>
{metadata.title}
</div>
{#if metadata.author}
<div class="video-author">
by {metadata.author}
</div>
{/if}
</div>
</div>
<ClipRangeSlider
min={0}
max={metadata.duration || 0}
step={0.001}
bind:start={clipStart}
bind:end={clipEnd}
/>
<div class="clip-time-display">
<div class="time-indicators">
<span class="start-time">{formatTime(clipStart)}</span>
<span class="duration-selected">
{formatTime(Math.max(0, clipEnd - clipStart))} selected
</span>
<span class="end-time">{formatTime(clipEnd)}</span>
</div>
</div>
{/if}
</div>
<style>
.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) {
.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;
}
}
</style>

View File

@ -37,6 +37,7 @@
import API from "$lib/api/api"; import API from "$lib/api/api";
import ClipRangeSlider from "./ClipRangeSlider.svelte"; import ClipRangeSlider from "./ClipRangeSlider.svelte";
import ClipCheckbox from "./buttons/ClipCheckbox.svelte"; import ClipCheckbox from "./buttons/ClipCheckbox.svelte";
import ClipControls from "./ClipControls.svelte";
let linkInput: Optional<HTMLInputElement>; let linkInput: Optional<HTMLInputElement>;
@ -284,60 +285,13 @@
</div> </div>
{#if clipMode && validLink($link)} {#if clipMode && validLink($link)}
<div class="clip-controls"> <ClipControls
{#if metaLoading} {metaLoading}
<div class="loading-state"> {metaError}
<div class="loading-spinner"></div> {metadata}
<span>Loading video metadata...</span> bind:clipStart
</div> bind:clipEnd
{:else if metaError}
<div class="error-state">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<span>{metaError}</span>
</div>
{:else if metadata}
<div class="clip-metadata">
<div class="metadata-header">
<h4>Clip</h4>
<div class="duration-badge">
{typeof metadata.duration === 'number' ? formatTime(metadata.duration) : 'Unknown'}
</div>
</div>
<div class="video-info">
<div class="video-title" title={metadata.title}>
{metadata.title}
</div>
{#if metadata.author}
<div class="video-author">
by {metadata.author}
</div>
{/if}
</div>
</div>
<ClipRangeSlider
min={0}
max={metadata.duration || 0}
step={0.001}
bind:start={clipStart}
bind:end={clipEnd}
/> />
<div class="clip-time-display">
<div class="time-indicators">
<span class="start-time">{formatTime(clipStart)}</span>
<span class="duration-selected">
{formatTime(Math.max(0, clipEnd - clipStart))} selected
</span>
<span class="end-time">{formatTime(clipEnd)}</span>
</div>
</div>
{/if}
</div>
{/if} {/if}
</div> </div>
@ -467,140 +421,6 @@
font-weight: 500; 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) { @media screen and (max-width: 440px) {
#action-container { #action-container {
flex-direction: column; flex-direction: column;
@ -618,36 +438,5 @@
#paste-desktop-text { #paste-desktop-text {
display: none; 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;
}
} }
</style> </style>

View File

@ -61,12 +61,11 @@
on:click={() => { on:click={() => {
hapticSwitch(); hapticSwitch();
if (clipMode) { if (clipMode) {
const req: CobaltSaveRequestBody & { clipStart?: number; clipEnd?: number } = { savingHandler({ request: {
url, url,
clipStart, clipStart,
clipEnd, clipEnd,
}; } });
savingHandler({ request: req });
} else { } else {
savingHandler({ url }); savingHandler({ url });
} }

View File

@ -113,7 +113,10 @@ export type CobaltServerInfo = {
// this allows for extra properties, which is not ideal, // this allows for extra properties, which is not ideal,
// but i couldn't figure out how to make a strict partial :( // but i couldn't figure out how to make a strict partial :(
export type CobaltSaveRequestBody = export type CobaltSaveRequestBody =
{ url: string } & Partial<Omit<CobaltSettings['save'], 'savingMethod'>>; { url: string } & Partial<Omit<CobaltSettings['save'], 'savingMethod'>> & {
clipStart?: number,
clipEnd?: number,
};
export type CobaltSessionResponse = CobaltSession | CobaltErrorResponse; export type CobaltSessionResponse = CobaltSession | CobaltErrorResponse;
export type CobaltServerInfoResponse = CobaltServerInfo | CobaltErrorResponse; export type CobaltServerInfoResponse = CobaltServerInfo | CobaltErrorResponse;