Fix Clip dialog to match Python version (v3.6.2)

- Added sliders for Start/End time selection
- Fixed filename format examples (DD.MM.YYYY_PartNum.mp4)
- Proper slider styling with orange color
- Sync between slider and text input
- Filename format without "Part" prefix in simple mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-02-04 14:12:22 +01:00
parent 2b935f4a3a
commit 023c1839ac
6 changed files with 270 additions and 55 deletions

View File

@ -0,0 +1,67 @@
{
"permissions": {
"allow": [
"Bash(ren \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch-YouTube\\\\Twitch\\\\Twitch_VOD_Manager_V_3.2.1.pyw\" \"Twitch_VOD_Manager_V_3.2.2.pyw\")",
"Bash(cmd /c ren:*)",
"Bash(npm install)",
"Bash(npm run build:win:*)",
"Bash(npm run build:portable:*)",
"Bash(npx electron-builder --win portable --config.win.signAndEditExecutable=false)",
"Bash(dir \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch-YouTube\\\\Twitch\\\\TwitchVODManager-Electron\\\\dist\")",
"Bash(python -m py_compile:*)",
"Bash(dir \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\*.exe\")",
"Bash(C:UsersploetAppDataRoamingPythonPython311Scriptspyinstaller.exe:*)",
"Bash(python -m PyInstaller:*)",
"Bash(dir \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\dist\\\\Twitch_VOD_Manager.exe\")",
"Bash(powershell -command:*)",
"Bash(python:*)",
"Bash(start \"\" \"Twitch_VOD_Manager.exe\")",
"Bash(tasklist:*)",
"Bash(findstr:*)",
"Bash(pip install:*)",
"Bash(taskkill:*)",
"Bash(ping:*)",
"Bash(move:*)",
"Bash(start Twitch_VOD_Manager.exe)",
"Bash(timeout:*)",
"Bash(\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe\" \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\installer.iss\")",
"Bash(\"/c/Users/ploet/AppData/Local/Programs/Inno Setup 6/ISCC.exe\" \"C:/Users/ploet/Desktop/Twitch Downloader/installer.iss\")",
"Bash(curl:*)",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch_VOD_Manager_Setup_test.exe\")",
"Bash(\"/c/Program Files/Twitch VOD Manager/unins000.exe\" /VERYSILENT /SUPPRESSMSGBOXES)",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch_VOD_Manager_Setup_NEW.exe\")",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.8.exe\")",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.10.exe\")",
"Bash(find:*)",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.12.exe\")",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.14.exe\")",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.16.exe\")",
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.18.exe\")",
"Bash(\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe\" installer.iss)",
"Bash(\"C:/Program Files \\(x86\\)/Inno Setup 6/ISCC.exe\" \"C:/Users/ploet/Desktop/Twitch Downloader/installer.iss\")",
"Bash(ls:*)",
"Bash(where:*)",
"Bash(cmd.exe /c \"\"\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe\"\" \"\"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\installer.iss\"\"\")",
"Bash(cmd.exe /c \"dir \"\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\"\"\")",
"Bash(powershell.exe -Command \"Test-Path ''C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe''\")",
"Bash(powershell.exe:*)",
"Bash(git init:*)",
"Bash(git add:*)",
"Bash(git rm:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nInitial commit: Twitch VOD Manager v3.5.3\n\n- Main application with auto-update functionality\n- PyInstaller spec for building standalone EXE\n- Inno Setup installer script with silent update support\n- Server version.json for update checking\n\nFeatures:\n- Download Twitch VODs\n- Auto-update with silent installation\n- Settings stored in ProgramData\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git config:*)",
"Bash(git commit:*)",
"Bash(gh auth:*)",
"Bash(npm run build:*)",
"Bash(npm run dist:win:*)",
"Bash(git push:*)",
"Bash(gh release create v3.6.0 \"release/Twitch VOD Manager Setup 3.6.0.exe\" --title \"Twitch VOD Manager v3.6.0\" --notes \"$\\(cat <<''EOF''\n## What''s New in v3.6.0\n\n### New Features\n- **Video Cutter** - Cut and trim your downloaded VODs with a visual timeline\n - Preview frames at any position\n - Set precise start and end times\n - Fast cutting using stream copy \\(no re-encoding\\)\n \n- **Video Merge** - Combine multiple video files into one\n - Add multiple videos and reorder them\n - Fast merging with stream copy\n \n- **Part-based Downloads** - Split long VODs into manageable segments\n - Configure segment length in settings\n - Automatically splits downloads into parts\n\n### Improvements\n- Enhanced download progress with speed and ETA display\n- New navigation tabs for better organization\n- Updated UI with new tool icons\n\n### Technical\n- FFmpeg/FFprobe integration for video processing\n- Improved IPC communication between main and renderer\n- Version bump to 3.6.0\nEOF\n\\)\")",
"Bash(powershell:*)",
"Bash(winget install:*)",
"Bash(\"C:\\\\Program Files\\\\GitHub CLI\\\\gh.exe\" auth status)",
"Bash(\"C:\\\\Program Files\\\\GitHub CLI\\\\gh.exe\" release create:*)",
"Bash(npm start)",
"Bash(\"C:\\\\Program Files\\\\GitHub CLI\\\\gh.exe\" release create v3.6.1 \"release/Twitch VOD Manager Setup 3.6.1.exe\" --title \"Twitch VOD Manager v3.6.1\" --notes \"## What''s New in v3.6.1\n\n### New Feature: Clip erstellen\n- **Time-Range Downloads** - Download specific portions of VODs\n - Click ''Clip'' button on any VOD\n - Use sliders or time inputs to select start/end times\n - Set custom part numbers for continuations\n - Downloads only the selected time range\n\n### All Features \\(v3.6.x\\)\n- VOD Downloads \\(full or part-based\\)\n- Clip Downloads from Twitch\n- Video Cutter for local files \\(FFmpeg\\)\n- Video Merge \\(combine multiple videos\\)\n- Time-Range VOD Downloads \\(new\\)\n- Multiple Themes \\(Twitch/Discord/YouTube/Apple\\)\n- Auto-Update Check\n\n### Technical\n- Uses streamlink --hls-start-offset and --hls-duration for precise downloads\n- CustomClip data structure for queue items\")"
]
}
}

View File

@ -0,0 +1,49 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = []
binaries = []
hiddenimports = []
tmp_ret = collect_all('customtkinter')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('cv2')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('imageio_ffmpeg')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['Twitch_VOD_Manager_V_3.3.4.pyw'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='Twitch_VOD_Manager_debug',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "3.6.1", "version": "3.6.2",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -959,24 +959,30 @@
font-size: 13px; font-size: 13px;
} }
.slider-group input[type="range"] { .slider-group input[type="range"],
.modal input[type="range"] {
width: 100%; width: 100%;
height: 6px; height: 6px;
-webkit-appearance: none; -webkit-appearance: none;
background: var(--bg-main); background: #1a1a1a;
border-radius: 3px; border-radius: 3px;
outline: none; outline: none;
} }
.slider-group input[type="range"]::-webkit-slider-thumb { .slider-group input[type="range"]::-webkit-slider-thumb,
.modal input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
width: 16px; width: 16px;
height: 16px; height: 16px;
background: var(--accent); background: #E5A00D;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
} }
.modal input[type="range"]::-webkit-slider-thumb:hover {
background: #ffb825;
}
.clip-time-display { .clip-time-display {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -1049,41 +1055,71 @@
<!-- Clip Dialog Modal --> <!-- Clip Dialog Modal -->
<div class="modal-overlay" id="clipModal"> <div class="modal-overlay" id="clipModal">
<div class="modal"> <div class="modal" style="background: #2b2b2b; max-width: 500px;">
<button class="modal-close" onclick="closeClipDialog()">x</button> <button class="modal-close" onclick="closeClipDialog()">x</button>
<h2>Clip erstellen</h2> <h2 style="color: #E5A00D; text-align: center; margin-bottom: 20px;" id="clipDialogTitle">Clip zuschneiden</h2>
<p style="color: var(--text-secondary); margin-bottom: 15px;" id="clipDialogTitle"></p>
<div class="slider-group"> <!-- Start Zeit mit Slider -->
<label>Startzeit</label> <div style="margin-bottom: 15px;">
<input type="range" id="clipStartSlider" min="0" max="100" value="0" oninput="updateClipSliders()"> <label style="display: block; margin-bottom: 5px;">Start:</label>
<div class="clip-time-display"> <input type="range" id="clipStartSlider" min="0" max="100" value="0"
<input type="text" id="clipStartTime" value="00:00:00" style="width: 80px; background: var(--bg-main); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 4px 8px; color: var(--text); text-align: center; font-family: monospace;" onchange="updateClipFromInput()"> style="width: 100%; height: 6px; -webkit-appearance: none; background: #1a1a1a; border-radius: 3px; cursor: pointer;"
oninput="updateFromSlider('start')">
<div style="display: flex; align-items: center; gap: 10px; margin-top: 8px;">
<label style="color: #888;">Startzeit (HH:MM:SS):</label>
<input type="text" id="clipStartTime" value="00:00:00"
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; color: white; font-family: monospace; text-align: center;"
onchange="updateFromInput('start')">
</div> </div>
</div> </div>
<div class="slider-group"> <!-- End Zeit mit Slider -->
<label>Endzeit</label> <div style="margin-bottom: 15px;">
<input type="range" id="clipEndSlider" min="0" max="100" value="100" oninput="updateClipSliders()"> <label style="display: block; margin-bottom: 5px;">Ende:</label>
<div class="clip-time-display"> <input type="range" id="clipEndSlider" min="0" max="100" value="60"
<input type="text" id="clipEndTime" value="00:00:00" style="width: 80px; background: var(--bg-main); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 4px 8px; color: var(--text); text-align: center; font-family: monospace;" onchange="updateClipFromInput()"> style="width: 100%; height: 6px; -webkit-appearance: none; background: #1a1a1a; border-radius: 3px; cursor: pointer;"
oninput="updateFromSlider('end')">
<div style="display: flex; align-items: center; gap: 10px; margin-top: 8px;">
<label style="color: #888;">Endzeit (HH:MM:SS):</label>
<input type="text" id="clipEndTime" value="00:01:00"
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 6px 10px; color: white; font-family: monospace; text-align: center;"
onchange="updateFromInput('end')">
</div> </div>
</div> </div>
<div class="clip-info-row"> <!-- Dauer Anzeige -->
<div class="label">Clip Dauer</div> <div style="text-align: center; margin-bottom: 20px;">
<div class="value" id="clipDurationDisplay">00:00:00</div> <span style="color: #888;">Dauer: </span>
<span id="clipDurationDisplay" style="color: #00c853;">00:01:00</span>
</div> </div>
<div class="part-number-group"> <!-- Part Nummer -->
<label style="display: block; margin-bottom: 6px; color: var(--text-secondary); font-size: 13px;">Start Part-Nummer (optional)</label> <div style="margin-bottom: 15px;">
<input type="number" id="clipStartPart" value="1" min="1"> <label style="display: block; margin-bottom: 8px;">Start Part-Nummer (optional, fur Fortsetzung):</label>
<small>Fur Fortsetzung eines Downloads</small> <input type="text" id="clipStartPart" placeholder="z.B. 42"
style="width: 100px; background: #333; border: 1px solid #444; border-radius: 4px; padding: 8px 12px; color: white;"
oninput="updateFilenameExamples()">
<div style="color: #888; font-size: 12px; margin-top: 5px;">Leer lassen = Part 1</div>
</div> </div>
<div class="modal-actions"> <!-- Dateinamen Format -->
<button class="btn-secondary" onclick="closeClipDialog()">Abbrechen</button> <div style="margin-bottom: 20px;">
<button class="btn-primary" id="clipConfirmBtn" onclick="confirmClipDialog()">Zur Queue hinzufugen</button> <label style="display: block; margin-bottom: 10px;">Dateinamen-Format:</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
<input type="radio" name="filenameFormat" value="simple" checked
style="width: 18px; height: 18px; accent-color: #9146FF;">
<span id="formatSimple" style="color: #aaa;">01.02.2026_1.mp4 (Standard)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="radio" name="filenameFormat" value="timestamp"
style="width: 18px; height: 18px; accent-color: #9146FF;">
<span id="formatTimestamp" style="color: #aaa;">01.02.2026_CLIP_00-00-00_1.mp4 (mit Zeitstempel)</span>
</label>
</div>
<!-- Button -->
<div style="text-align: center;">
<button class="btn-primary" style="background: #00c853; padding: 12px 30px; border: none; border-radius: 4px; color: white; font-weight: 600; cursor: pointer;" onclick="confirmClipDialog()">Zur Queue hinzufugen</button>
</div> </div>
</div> </div>
</div> </div>
@ -1334,7 +1370,7 @@
<div class="settings-card"> <div class="settings-card">
<h3>Updates</h3> <h3>Updates</h3>
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.6.1</p> <p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.6.2</p>
<button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button> <button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button>
</div> </div>
</div> </div>
@ -1345,7 +1381,7 @@
<div class="status-dot" id="statusDot"></div> <div class="status-dot" id="statusDot"></div>
<span id="statusText">Nicht verbunden</span> <span id="statusText">Nicht verbunden</span>
</div> </div>
<span id="versionText">v3.6.1</span> <span id="versionText">v3.6.2</span>
</div> </div>
</main> </main>
</div> </div>
@ -1635,6 +1671,13 @@
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
} }
function formatSecondsToTimeDashed(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) { function parseTimeToSeconds(timeStr) {
const parts = timeStr.split(':').map(p => parseInt(p) || 0); const parts = timeStr.split(':').map(p => parseInt(p) || 0);
if (parts.length === 3) { if (parts.length === 3) {
@ -1647,14 +1690,20 @@
clipDialogData = { url, title, date, streamer, duration }; clipDialogData = { url, title, date, streamer, duration };
clipTotalSeconds = parseDurationToSeconds(duration); clipTotalSeconds = parseDurationToSeconds(duration);
document.getElementById('clipDialogTitle').textContent = title.substring(0, 50) + (title.length > 50 ? '...' : '') + ' (' + duration + ')'; document.getElementById('clipDialogTitle').textContent = 'Clip zuschneiden (' + duration + ')';
// Setup sliders
document.getElementById('clipStartSlider').max = clipTotalSeconds; document.getElementById('clipStartSlider').max = clipTotalSeconds;
document.getElementById('clipEndSlider').max = clipTotalSeconds; document.getElementById('clipEndSlider').max = clipTotalSeconds;
document.getElementById('clipStartSlider').value = 0; document.getElementById('clipStartSlider').value = 0;
document.getElementById('clipEndSlider').value = Math.min(3600, clipTotalSeconds); // Default 1 hour or max document.getElementById('clipEndSlider').value = Math.min(60, clipTotalSeconds);
document.getElementById('clipStartPart').value = 1;
updateClipSliders(); document.getElementById('clipStartTime').value = '00:00:00';
document.getElementById('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds));
document.getElementById('clipStartPart').value = '';
updateClipDuration();
updateFilenameExamples();
document.getElementById('clipModal').classList.add('show'); document.getElementById('clipModal').classList.add('show');
} }
@ -1663,47 +1712,81 @@
clipDialogData = null; clipDialogData = null;
} }
function updateClipSliders() { function updateFromSlider(which) {
const startSec = parseInt(document.getElementById('clipStartSlider').value); const startSlider = document.getElementById('clipStartSlider');
const endSec = parseInt(document.getElementById('clipEndSlider').value); const endSlider = document.getElementById('clipEndSlider');
document.getElementById('clipStartTime').value = formatSecondsToTime(startSec); if (which === 'start') {
document.getElementById('clipEndTime').value = formatSecondsToTime(endSec); document.getElementById('clipStartTime').value = formatSecondsToTime(parseInt(startSlider.value));
} else {
document.getElementById('clipEndTime').value = formatSecondsToTime(parseInt(endSlider.value));
}
updateClipDuration();
}
function updateFromInput(which) {
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
if (which === 'start') {
document.getElementById('clipStartSlider').value = Math.max(0, Math.min(startSec, clipTotalSeconds));
} else {
document.getElementById('clipEndSlider').value = Math.max(0, Math.min(endSec, clipTotalSeconds));
}
updateClipDuration();
}
function updateClipDuration() {
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
const duration = endSec - startSec; const duration = endSec - startSec;
const durationDisplay = document.getElementById('clipDurationDisplay'); const durationDisplay = document.getElementById('clipDurationDisplay');
if (duration > 0) { if (duration > 0) {
durationDisplay.textContent = formatSecondsToTime(duration); durationDisplay.textContent = formatSecondsToTime(duration);
durationDisplay.classList.remove('error'); durationDisplay.style.color = '#00c853';
} else { } else {
durationDisplay.textContent = 'Ungultig'; durationDisplay.textContent = 'Ungultig!';
durationDisplay.classList.add('error'); durationDisplay.style.color = '#ff4444';
}
} }
function updateClipFromInput() { updateFilenameExamples();
}
function updateFilenameExamples() {
if (!clipDialogData) return;
const date = new Date(clipDialogData.date);
const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`;
const partNum = document.getElementById('clipStartPart').value || '1';
const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value); const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value); const timeStr = formatSecondsToTimeDashed(startSec);
document.getElementById('clipStartSlider').value = Math.max(0, Math.min(startSec, clipTotalSeconds)); document.getElementById('formatSimple').textContent = `${dateStr}_${partNum}.mp4 (Standard)`;
document.getElementById('clipEndSlider').value = Math.max(0, Math.min(endSec, clipTotalSeconds)); document.getElementById('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 (mit Zeitstempel)`;
updateClipSliders();
} }
async function confirmClipDialog() { async function confirmClipDialog() {
if (!clipDialogData) return; if (!clipDialogData) return;
const startSec = parseInt(document.getElementById('clipStartSlider').value); const startSec = parseTimeToSeconds(document.getElementById('clipStartTime').value);
const endSec = parseInt(document.getElementById('clipEndSlider').value); const endSec = parseTimeToSeconds(document.getElementById('clipEndTime').value);
const startPart = parseInt(document.getElementById('clipStartPart').value) || 1; const startPartStr = document.getElementById('clipStartPart').value.trim();
const startPart = startPartStr ? parseInt(startPartStr) : 1;
const filenameFormat = document.querySelector('input[name="filenameFormat"]:checked').value;
if (endSec <= startSec) { if (endSec <= startSec) {
alert('Endzeit muss grosser als Startzeit sein!'); alert('Endzeit muss grosser als Startzeit sein!');
return; return;
} }
if (startSec < 0 || endSec > clipTotalSeconds) {
alert('Zeit ausserhalb des VOD-Bereichs!');
return;
}
const durationSec = endSec - startSec; const durationSec = endSec - startSec;
queue = await window.api.addToQueue({ queue = await window.api.addToQueue({
@ -1715,7 +1798,8 @@
customClip: { customClip: {
startSec: startSec, startSec: startSec,
durationSec: durationSec, durationSec: durationSec,
startPart: startPart startPart: startPart,
filenameFormat: filenameFormat
} }
}); });

View File

@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
// ========================================== // ==========================================
// CONFIG & CONSTANTS // CONFIG & CONSTANTS
// ========================================== // ==========================================
const APP_VERSION = '3.6.1'; const APP_VERSION = '3.6.2';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths // Paths
@ -55,6 +55,7 @@ interface CustomClip {
startSec: number; startSec: number;
durationSec: number; durationSec: number;
startPart: number; startPart: number;
filenameFormat: 'simple' | 'timestamp';
} }
interface QueueItem { interface QueueItem {
@ -681,6 +682,19 @@ async function downloadVOD(
const clip = item.customClip; const clip = item.customClip;
const partDuration = config.part_minutes * 60; const partDuration = config.part_minutes * 60;
// Helper to generate filename based on format
const makeClipFilename = (partNum: number, startOffset: number): string => {
if (clip.filenameFormat === 'timestamp') {
const h = Math.floor(startOffset / 3600);
const m = Math.floor((startOffset % 3600) / 60);
const s = Math.floor(startOffset % 60);
const timeStr = `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
return path.join(folder, `${dateStr}_CLIP_${timeStr}_${partNum}.mp4`);
} else {
return path.join(folder, `${dateStr}_${partNum}.mp4`);
}
};
// If clip is longer than part duration, split into parts // If clip is longer than part duration, split into parts
if (clip.durationSec > partDuration) { if (clip.durationSec > partDuration) {
const numParts = Math.ceil(clip.durationSec / partDuration); const numParts = Math.ceil(clip.durationSec / partDuration);
@ -694,7 +708,7 @@ async function downloadVOD(
const remainingDuration = clip.durationSec - (i * partDuration); const remainingDuration = clip.durationSec - (i * partDuration);
const thisDuration = Math.min(partDuration, remainingDuration); const thisDuration = Math.min(partDuration, remainingDuration);
const partFilename = path.join(folder, `${dateStr}_Part${partNum.toString().padStart(2, '0')}.mp4`); const partFilename = makeClipFilename(partNum, startOffset);
const success = await downloadVODPart( const success = await downloadVODPart(
item.url, item.url,
@ -714,7 +728,7 @@ async function downloadVOD(
return downloadedFiles.length === numParts; return downloadedFiles.length === numParts;
} else { } else {
// Single clip file // Single clip file
const filename = path.join(folder, `${dateStr}_Part${clip.startPart.toString().padStart(2, '0')}.mp4`); const filename = makeClipFilename(clip.startPart, clip.startSec);
return await downloadVODPart( return await downloadVODPart(
item.url, item.url,
filename, filename,

View File

@ -5,6 +5,7 @@ interface CustomClip {
startSec: number; startSec: number;
durationSec: number; durationSec: number;
startPart: number; startPart: number;
filenameFormat: 'simple' | 'timestamp';
} }
interface QueueItem { interface QueueItem {