diff --git a/typescript-version/package.json b/typescript-version/package.json
index 30fe7ae..dd85a9b 100644
--- a/typescript-version/package.json
+++ b/typescript-version/package.json
@@ -1,6 +1,6 @@
{
"name": "twitch-vod-manager",
- "version": "3.6.0",
+ "version": "3.6.1",
"description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js",
"author": "xRangerDE",
diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html
index 2fbbdc9..5dc5a77 100644
--- a/typescript-version/src/index.html
+++ b/typescript-version/src/index.html
@@ -899,6 +899,146 @@
--accent: #0A84FF;
--accent-hover: #0071e3;
}
+
+ /* Modal Styles */
+ .modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: none;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ }
+
+ .modal-overlay.show {
+ display: flex;
+ }
+
+ .modal {
+ background: var(--bg-card);
+ border-radius: 12px;
+ padding: 25px;
+ width: 90%;
+ max-width: 500px;
+ max-height: 90vh;
+ overflow-y: auto;
+ }
+
+ .modal h2 {
+ margin-bottom: 20px;
+ font-size: 18px;
+ }
+
+ .modal-close {
+ float: right;
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 24px;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+ }
+
+ .modal-close:hover {
+ color: var(--text);
+ }
+
+ .slider-group {
+ margin-bottom: 20px;
+ }
+
+ .slider-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+ font-size: 13px;
+ }
+
+ .slider-group input[type="range"] {
+ width: 100%;
+ height: 6px;
+ -webkit-appearance: none;
+ background: var(--bg-main);
+ border-radius: 3px;
+ outline: none;
+ }
+
+ .slider-group input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ background: var(--accent);
+ border-radius: 50%;
+ cursor: pointer;
+ }
+
+ .clip-time-display {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+ font-family: monospace;
+ font-size: 14px;
+ }
+
+ .clip-info-row {
+ background: var(--bg-main);
+ padding: 12px 15px;
+ border-radius: 6px;
+ margin-bottom: 15px;
+ text-align: center;
+ }
+
+ .clip-info-row .label {
+ color: var(--text-secondary);
+ font-size: 12px;
+ margin-bottom: 4px;
+ }
+
+ .clip-info-row .value {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--success);
+ }
+
+ .clip-info-row .value.error {
+ color: var(--error);
+ }
+
+ .part-number-group {
+ margin-bottom: 20px;
+ }
+
+ .part-number-group input {
+ width: 100px;
+ background: var(--bg-main);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 4px;
+ padding: 8px 12px;
+ color: var(--text);
+ font-size: 14px;
+ }
+
+ .part-number-group small {
+ display: block;
+ margin-top: 5px;
+ color: var(--text-secondary);
+ font-size: 11px;
+ }
+
+ .modal-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 20px;
+ }
+
+ .modal-actions button {
+ flex: 1;
+ }
@@ -907,6 +1047,47 @@
+
+
+
+
+
Clip erstellen
+
+
+
+
+
+
+
+
Clip Dauer
+
00:00:00
+
+
+
+
+
+ Fur Fortsetzung eines Downloads
+
+
+
+
+
+
+
+
+
- v3.6.0
+ v3.6.1
@@ -1360,6 +1541,7 @@
grid.innerHTML = vods.map(vod => {
const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180');
const date = new Date(vod.created_at).toLocaleDateString('de-DE');
+ const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/"/g, """);
return `

@@ -1372,7 +1554,8 @@
-
+
+
`;
@@ -1410,13 +1593,16 @@
return;
}
- list.innerHTML = queue.map(item => `
-
- `).join('');
+ list.innerHTML = queue.map(item => {
+ const isClip = item.customClip ? '* ' : '';
+ return `
+
+
+
${isClip}${item.title}
+
x
+
+ `;
+ }).join('');
}
async function toggleDownload() {
@@ -1427,6 +1613,116 @@
}
}
+ // Clip Dialog
+ let clipDialogData = null;
+ let clipTotalSeconds = 0;
+
+ function parseDurationToSeconds(durStr) {
+ let seconds = 0;
+ const hours = durStr.match(/(\d+)h/);
+ const minutes = durStr.match(/(\d+)m/);
+ const secs = durStr.match(/(\d+)s/);
+ if (hours) seconds += parseInt(hours[1]) * 3600;
+ if (minutes) seconds += parseInt(minutes[1]) * 60;
+ if (secs) seconds += parseInt(secs[1]);
+ return seconds;
+ }
+
+ function formatSecondsToTime(seconds) {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = Math.floor(seconds % 60);
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
+ }
+
+ function parseTimeToSeconds(timeStr) {
+ const parts = timeStr.split(':').map(p => parseInt(p) || 0);
+ if (parts.length === 3) {
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
+ }
+ return 0;
+ }
+
+ function openClipDialog(url, title, date, streamer, duration) {
+ clipDialogData = { url, title, date, streamer, duration };
+ clipTotalSeconds = parseDurationToSeconds(duration);
+
+ document.getElementById('clipDialogTitle').textContent = title.substring(0, 50) + (title.length > 50 ? '...' : '') + ' (' + duration + ')';
+ document.getElementById('clipStartSlider').max = clipTotalSeconds;
+ document.getElementById('clipEndSlider').max = clipTotalSeconds;
+ document.getElementById('clipStartSlider').value = 0;
+ document.getElementById('clipEndSlider').value = Math.min(3600, clipTotalSeconds); // Default 1 hour or max
+ document.getElementById('clipStartPart').value = 1;
+
+ updateClipSliders();
+ document.getElementById('clipModal').classList.add('show');
+ }
+
+ function closeClipDialog() {
+ document.getElementById('clipModal').classList.remove('show');
+ clipDialogData = null;
+ }
+
+ function updateClipSliders() {
+ const startSec = parseInt(document.getElementById('clipStartSlider').value);
+ const endSec = parseInt(document.getElementById('clipEndSlider').value);
+
+ document.getElementById('clipStartTime').value = formatSecondsToTime(startSec);
+ document.getElementById('clipEndTime').value = formatSecondsToTime(endSec);
+
+ const duration = endSec - startSec;
+ const durationDisplay = document.getElementById('clipDurationDisplay');
+
+ if (duration > 0) {
+ durationDisplay.textContent = formatSecondsToTime(duration);
+ durationDisplay.classList.remove('error');
+ } else {
+ durationDisplay.textContent = 'Ungultig';
+ durationDisplay.classList.add('error');
+ }
+ }
+
+ function updateClipFromInput() {
+ const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
+ const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
+
+ document.getElementById('clipStartSlider').value = Math.max(0, Math.min(startSec, clipTotalSeconds));
+ document.getElementById('clipEndSlider').value = Math.max(0, Math.min(endSec, clipTotalSeconds));
+
+ updateClipSliders();
+ }
+
+ async function confirmClipDialog() {
+ if (!clipDialogData) return;
+
+ const startSec = parseInt(document.getElementById('clipStartSlider').value);
+ const endSec = parseInt(document.getElementById('clipEndSlider').value);
+ const startPart = parseInt(document.getElementById('clipStartPart').value) || 1;
+
+ if (endSec <= startSec) {
+ alert('Endzeit muss grosser als Startzeit sein!');
+ return;
+ }
+
+ const durationSec = endSec - startSec;
+
+ queue = await window.api.addToQueue({
+ url: clipDialogData.url,
+ title: clipDialogData.title,
+ date: clipDialogData.date,
+ streamer: clipDialogData.streamer,
+ duration_str: clipDialogData.duration,
+ customClip: {
+ startSec: startSec,
+ durationSec: durationSec,
+ startPart: startPart
+ }
+ });
+
+ renderQueue();
+ closeClipDialog();
+ }
+
// Settings
async function saveSettings() {
const clientId = document.getElementById('clientId').value;
diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts
index 8c1625c..dff0d48 100644
--- a/typescript-version/src/main.ts
+++ b/typescript-version/src/main.ts
@@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
// ==========================================
// CONFIG & CONSTANTS
// ==========================================
-const APP_VERSION = '3.6.0';
+const APP_VERSION = '3.6.1';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths
@@ -51,6 +51,12 @@ interface VOD {
stream_id: string;
}
+interface CustomClip {
+ startSec: number;
+ durationSec: number;
+ startPart: number;
+}
+
interface QueueItem {
id: string;
title: string;
@@ -66,6 +72,7 @@ interface QueueItem {
eta?: string;
downloadedBytes?: number;
totalBytes?: number;
+ customClip?: CustomClip;
}
interface DownloadProgress {
@@ -669,6 +676,58 @@ async function downloadVOD(
const safeTitle = item.title.replace(/[^a-zA-Z0-9_\- ]/g, '').substring(0, 50);
const totalDuration = parseDuration(item.duration_str);
+ // Custom Clip - download specific time range
+ if (item.customClip) {
+ const clip = item.customClip;
+ const partDuration = config.part_minutes * 60;
+
+ // If clip is longer than part duration, split into parts
+ if (clip.durationSec > partDuration) {
+ const numParts = Math.ceil(clip.durationSec / partDuration);
+ const downloadedFiles: string[] = [];
+
+ for (let i = 0; i < numParts; i++) {
+ if (currentDownloadCancelled) break;
+
+ const partNum = clip.startPart + i;
+ const startOffset = clip.startSec + (i * partDuration);
+ const remainingDuration = clip.durationSec - (i * partDuration);
+ const thisDuration = Math.min(partDuration, remainingDuration);
+
+ const partFilename = path.join(folder, `${dateStr}_Part${partNum.toString().padStart(2, '0')}.mp4`);
+
+ const success = await downloadVODPart(
+ item.url,
+ partFilename,
+ formatDuration(startOffset),
+ formatDuration(thisDuration),
+ onProgress,
+ item.id,
+ i + 1,
+ numParts
+ );
+
+ if (!success) return false;
+ downloadedFiles.push(partFilename);
+ }
+
+ return downloadedFiles.length === numParts;
+ } else {
+ // Single clip file
+ const filename = path.join(folder, `${dateStr}_Part${clip.startPart.toString().padStart(2, '0')}.mp4`);
+ return await downloadVODPart(
+ item.url,
+ filename,
+ formatDuration(clip.startSec),
+ formatDuration(clip.durationSec),
+ onProgress,
+ item.id,
+ 1,
+ 1
+ );
+ }
+ }
+
// Check download mode
if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) {
// Full download
@@ -687,7 +746,7 @@ async function downloadVOD(
const endSec = Math.min((i + 1) * partDuration, totalDuration);
const duration = endSec - startSec;
- const partFilename = path.join(folder, `${safeTitle}_Part${(i + 1).toString().padStart(2, '0')}.mp4`);
+ const partFilename = path.join(folder, `${dateStr}_Part${(i + 1).toString().padStart(2, '0')}.mp4`);
const success = await downloadVODPart(
item.url,
diff --git a/typescript-version/src/preload.ts b/typescript-version/src/preload.ts
index 5420e66..8f717eb 100644
--- a/typescript-version/src/preload.ts
+++ b/typescript-version/src/preload.ts
@@ -1,6 +1,12 @@
import { contextBridge, ipcRenderer } from 'electron';
// Types
+interface CustomClip {
+ startSec: number;
+ durationSec: number;
+ startPart: number;
+}
+
interface QueueItem {
id: string;
title: string;
@@ -14,6 +20,7 @@ interface QueueItem {
totalParts?: number;
speed?: string;
eta?: string;
+ customClip?: CustomClip;
}
interface DownloadProgress {