Skip to content

Commit c6aee3f

Browse files
committed
feat: add batch URL downloads, analytics dashboard, and duration limit
- Add batch URL input support (paste multiple URLs, one per line) - New /api/downloads/batch endpoint (up to 100 URLs) - Add Analytics dashboard in admin settings - Overview stats (total, completed, failed, success rate) - Storage usage visualization - Top uploaders and active subscriptions - Downloads per day chart (30 days) - Downloads by format breakdown - Add max duration limit setting (default 3 hours) - Configurable in admin settings - Validates during metadata fetch - 0 = no limit - Update .dockerignore with better organization - Add Dependabot for automated dependency updates
1 parent cc8c672 commit c6aee3f

10 files changed

Lines changed: 812 additions & 26 deletions

File tree

.dockerignore

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,50 @@
1+
# Dependencies
12
node_modules
23
npm-debug.log
4+
yarn-debug.log
5+
pnpm-debug.log
6+
7+
# Environment files
38
.env
49
.env.*
510
!.env.example
11+
12+
# Git
613
.git
714
.gitignore
815
.github
16+
17+
# IDEs
918
.vscode
1019
.idea
20+
*.swp
21+
*.swo
22+
23+
# Documentation (except README)
1124
*.md
1225
!README.md
26+
LICENSE
27+
28+
# Docker files
1329
Dockerfile*
1430
docker-compose*
31+
32+
# Build outputs
1533
.svelte-kit
1634
build
17-
wytui
18-
.DS_Store
35+
dist
1936
coverage
37+
38+
# Logs
2039
*.log
21-
dist
22-
LICENSE
40+
logs
41+
42+
# OS files
43+
.DS_Store
44+
Thumbs.db
45+
46+
# Test data
47+
wytui
48+
49+
# Claude Code
50+
.claude

.github/dependabot.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
version: 2
2+
updates:
3+
# Enable version updates for npm
4+
- package-ecosystem: "npm"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
day: "monday"
9+
time: "09:00"
10+
open-pull-requests-limit: 10
11+
groups:
12+
# Group all non-major updates together
13+
minor-and-patch:
14+
patterns:
15+
- "*"
16+
update-types:
17+
- "minor"
18+
- "patch"
19+
# Group development dependencies
20+
dev-dependencies:
21+
dependency-type: "development"
22+
update-types:
23+
- "minor"
24+
- "patch"
25+
# Specific package configurations
26+
ignore:
27+
# Ignore major version updates for stable dependencies
28+
# (uncomment and add packages as needed)
29+
# - dependency-name: "svelte"
30+
# update-types: ["version-update:semver-major"]
31+
labels:
32+
- "dependencies"
33+
- "automated"
34+
reviewers:
35+
- "willuhmjs"
36+
commit-message:
37+
prefix: "chore"
38+
include: "scope"
39+
40+
# Enable version updates for Docker
41+
- package-ecosystem: "docker"
42+
directory: "/"
43+
schedule:
44+
interval: "weekly"
45+
day: "monday"
46+
labels:
47+
- "dependencies"
48+
- "docker"
49+
commit-message:
50+
prefix: "chore"
51+
52+
# Enable version updates for GitHub Actions
53+
- package-ecosystem: "github-actions"
54+
directory: "/"
55+
schedule:
56+
interval: "weekly"
57+
day: "monday"
58+
labels:
59+
- "dependencies"
60+
- "ci"
61+
commit-message:
62+
prefix: "chore"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "settings" ADD COLUMN "maxDurationSeconds" INTEGER DEFAULT 10800;

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ model Settings {
284284
jellyfinUrl String?
285285
jellyfinApiKey String?
286286
287+
// Download limits
288+
maxDurationSeconds Int? @default(10800) // 3 hours default
289+
287290
createdAt DateTime @default(now())
288291
updatedAt DateTime @updatedAt
289292

src/lib/components/download/DownloadForm.svelte

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,12 @@
848848
loading = true;
849849
850850
try {
851-
const body: any = { url, profileId: activeProfileId, saveToLibrary };
851+
// Parse URLs - one per line
852+
const urls = url.trim().split('\n')
853+
.map(line => line.trim())
854+
.filter(line => line.length > 0);
855+
856+
const body: any = { profileId: activeProfileId, saveToLibrary };
852857
if (advancedMode) {
853858
const cf = buildCustomFlags();
854859
if (cf.length > 0) body.customFlags = cf;
@@ -857,15 +862,32 @@
857862
if (bf.length > 0) body.customFlags = bf;
858863
}
859864
860-
const res = await fetch("/api/downloads", {
861-
method: "POST",
862-
headers: { "Content-Type": "application/json" },
863-
body: JSON.stringify(body),
864-
});
865-
866-
if (!res.ok) {
867-
const data = await res.json();
868-
throw new Error(data.message || "Failed to create download");
865+
// If single URL, use original API
866+
if (urls.length === 1) {
867+
body.url = urls[0];
868+
const res = await fetch("/api/downloads", {
869+
method: "POST",
870+
headers: { "Content-Type": "application/json" },
871+
body: JSON.stringify(body),
872+
});
873+
874+
if (!res.ok) {
875+
const data = await res.json();
876+
throw new Error(data.message || "Failed to create download");
877+
}
878+
} else {
879+
// Batch submission
880+
body.urls = urls;
881+
const res = await fetch("/api/downloads/batch", {
882+
method: "POST",
883+
headers: { "Content-Type": "application/json" },
884+
body: JSON.stringify(body),
885+
});
886+
887+
if (!res.ok) {
888+
const data = await res.json();
889+
throw new Error(data.message || "Failed to create batch download");
890+
}
869891
}
870892
871893
url = "";
@@ -1001,10 +1023,15 @@
10011023
<textarea
10021024
id="url"
10031025
bind:value={url}
1004-
placeholder="Paste YouTube, TikTok, Twitter, or any supported URL..."
1026+
placeholder="Paste YouTube, TikTok, Twitter, or any supported URL...&#10;&#10;Tip: Paste multiple URLs (one per line) for batch download"
10051027
rows="3"
10061028
disabled={loading}
10071029
></textarea>
1030+
{#if url.trim().split('\n').filter(line => line.trim()).length > 1}
1031+
<p class="batch-hint">
1032+
{url.trim().split('\n').filter(line => line.trim()).length} URLs detected
1033+
</p>
1034+
{/if}
10081035
</div>
10091036

10101037
{#if libraryConfigured}
@@ -1849,6 +1876,13 @@
18491876
margin-bottom: var(--spacing-md);
18501877
}
18511878
1879+
.batch-hint {
1880+
margin-top: var(--spacing-xs);
1881+
font-size: 0.75rem;
1882+
color: var(--accent-primary);
1883+
font-weight: 500;
1884+
}
1885+
18521886
button[type="submit"] {
18531887
width: 100%;
18541888
}

src/lib/server/services/download.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ class DownloadService {
156156
try {
157157
const metadata = await ytdlpService.fetchMetadata(download.url);
158158

159+
// Check duration limit
160+
const settings = await this.getSettings();
161+
if (settings.maxDurationSeconds && metadata.duration) {
162+
if (metadata.duration > settings.maxDurationSeconds) {
163+
throw new Error(
164+
`Video duration (${Math.round(metadata.duration / 60)} min) exceeds limit (${Math.round(settings.maxDurationSeconds / 60)} min)`
165+
);
166+
}
167+
}
168+
159169
const updated = await this.updateDownload(downloadId, {
160170
title: metadata.title,
161171
thumbnail: metadata.thumbnail,

0 commit comments

Comments
 (0)