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:
parent
2b935f4a3a
commit
023c1839ac
67
.claude/settings.local.json
Normal file
67
.claude/settings.local.json
Normal 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\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Twitch_VOD_Manager_Debug.spec
Normal file
49
Twitch_VOD_Manager_Debug.spec
Normal 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,
|
||||||
|
)
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFilenameExamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateClipFromInput() {
|
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user