Add global filename templates for VODs, parts, and clips (v4.0.5)
This commit is contained in:
parent
9845b25d03
commit
1306309e6e
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"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",
|
||||||
|
|||||||
@ -364,11 +364,25 @@
|
|||||||
<label id="partMinutesLabel">Teil-Lange (Minuten)</label>
|
<label id="partMinutesLabel">Teil-Lange (Minuten)</label>
|
||||||
<input type="number" id="partMinutes" value="120" min="10" max="480">
|
<input type="number" id="partMinutes" value="120" min="10" max="480">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="filenameTemplatesTitle">Dateinamen-Templates</label>
|
||||||
|
<div style="display: grid; gap: 8px; margin-top: 8px;">
|
||||||
|
<label id="vodTemplateLabel" style="font-size: 13px; color: var(--text-secondary);">VOD Template</label>
|
||||||
|
<input type="text" id="vodFilenameTemplate" placeholder="{title}.mp4" style="font-family: monospace;">
|
||||||
|
|
||||||
|
<label id="partsTemplateLabel" style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">VOD Part Template</label>
|
||||||
|
<input type="text" id="partsFilenameTemplate" placeholder="{date}_Part{part_padded}.mp4" style="font-family: monospace;">
|
||||||
|
|
||||||
|
<label id="defaultClipTemplateLabel" style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">Clip Template</label>
|
||||||
|
<input type="text" id="defaultClipFilenameTemplate" placeholder="{date}_{part}.mp4" style="font-family: monospace;">
|
||||||
|
</div>
|
||||||
|
<div id="filenameTemplateHint" style="color: #888; font-size: 12px; margin-top: 8px;">Platzhalter: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="updateTitle">Updates</h3>
|
<h3 id="updateTitle">Updates</h3>
|
||||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.4</p>
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.5</p>
|
||||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -400,7 +414,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">v4.0.4</span>
|
<span id="versionText">v4.0.5</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG & CONSTANTS
|
// CONFIG & CONSTANTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const APP_VERSION = '4.0.4';
|
const APP_VERSION = '4.0.5';
|
||||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
@ -20,6 +20,9 @@ const TOOLS_DIR = path.join(APPDATA_DIR, 'tools');
|
|||||||
const TOOLS_STREAMLINK_DIR = path.join(TOOLS_DIR, 'streamlink');
|
const TOOLS_STREAMLINK_DIR = path.join(TOOLS_DIR, 'streamlink');
|
||||||
const TOOLS_FFMPEG_DIR = path.join(TOOLS_DIR, 'ffmpeg');
|
const TOOLS_FFMPEG_DIR = path.join(TOOLS_DIR, 'ffmpeg');
|
||||||
const DEFAULT_DOWNLOAD_PATH = path.join(app.getPath('desktop'), 'Twitch_VODs');
|
const DEFAULT_DOWNLOAD_PATH = path.join(app.getPath('desktop'), 'Twitch_VODs');
|
||||||
|
const DEFAULT_FILENAME_TEMPLATE_VOD = '{title}.mp4';
|
||||||
|
const DEFAULT_FILENAME_TEMPLATE_PARTS = '{date}_Part{part_padded}.mp4';
|
||||||
|
const DEFAULT_FILENAME_TEMPLATE_CLIP = '{date}_{part}.mp4';
|
||||||
|
|
||||||
// Timeouts
|
// Timeouts
|
||||||
const API_TIMEOUT = 10000;
|
const API_TIMEOUT = 10000;
|
||||||
@ -44,6 +47,9 @@ interface Config {
|
|||||||
download_mode: 'parts' | 'full';
|
download_mode: 'parts' | 'full';
|
||||||
part_minutes: number;
|
part_minutes: number;
|
||||||
language: 'de' | 'en';
|
language: 'de' | 'en';
|
||||||
|
filename_template_vod: string;
|
||||||
|
filename_template_parts: string;
|
||||||
|
filename_template_clip: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VOD {
|
interface VOD {
|
||||||
@ -135,19 +141,36 @@ const defaultConfig: Config = {
|
|||||||
theme: 'twitch',
|
theme: 'twitch',
|
||||||
download_mode: 'full',
|
download_mode: 'full',
|
||||||
part_minutes: 120,
|
part_minutes: 120,
|
||||||
language: 'en'
|
language: 'en',
|
||||||
|
filename_template_vod: DEFAULT_FILENAME_TEMPLATE_VOD,
|
||||||
|
filename_template_parts: DEFAULT_FILENAME_TEMPLATE_PARTS,
|
||||||
|
filename_template_clip: DEFAULT_FILENAME_TEMPLATE_CLIP
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeFilenameTemplate(template: string | undefined, fallback: string): string {
|
||||||
|
const value = (template || '').trim();
|
||||||
|
return value || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeConfigTemplates(input: Config): Config {
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
filename_template_vod: normalizeFilenameTemplate(input.filename_template_vod, DEFAULT_FILENAME_TEMPLATE_VOD),
|
||||||
|
filename_template_parts: normalizeFilenameTemplate(input.filename_template_parts, DEFAULT_FILENAME_TEMPLATE_PARTS),
|
||||||
|
filename_template_clip: normalizeFilenameTemplate(input.filename_template_clip, DEFAULT_FILENAME_TEMPLATE_CLIP)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function loadConfig(): Config {
|
function loadConfig(): Config {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||||
return { ...defaultConfig, ...JSON.parse(data) };
|
return normalizeConfigTemplates({ ...defaultConfig, ...JSON.parse(data) });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading config:', e);
|
console.error('Error loading config:', e);
|
||||||
}
|
}
|
||||||
return defaultConfig;
|
return normalizeConfigTemplates(defaultConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConfig(config: Config): void {
|
function saveConfig(config: Config): void {
|
||||||
@ -697,6 +720,7 @@ interface ClipTemplateContext {
|
|||||||
channel: string;
|
channel: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
part: number;
|
part: number;
|
||||||
|
partPadded: string;
|
||||||
trimStartSec: number;
|
trimStartSec: number;
|
||||||
trimEndSec: number;
|
trimEndSec: number;
|
||||||
trimLengthSec: number;
|
trimLengthSec: number;
|
||||||
@ -712,6 +736,7 @@ function renderClipFilenameTemplate(context: ClipTemplateContext): string {
|
|||||||
.replace(/\{channel_id\}/g, '')
|
.replace(/\{channel_id\}/g, '')
|
||||||
.replace(/\{date\}/g, baseDate)
|
.replace(/\{date\}/g, baseDate)
|
||||||
.replace(/\{part\}/g, String(context.part))
|
.replace(/\{part\}/g, String(context.part))
|
||||||
|
.replace(/\{part_padded\}/g, context.partPadded)
|
||||||
.replace(/\{trim_start\}/g, formatDurationDashed(context.trimStartSec))
|
.replace(/\{trim_start\}/g, formatDurationDashed(context.trimStartSec))
|
||||||
.replace(/\{trim_end\}/g, formatDurationDashed(context.trimEndSec))
|
.replace(/\{trim_end\}/g, formatDurationDashed(context.trimEndSec))
|
||||||
.replace(/\{trim_length\}/g, formatDurationDashed(context.trimLengthSec))
|
.replace(/\{trim_length\}/g, formatDurationDashed(context.trimLengthSec))
|
||||||
@ -1448,8 +1473,32 @@ async function downloadVOD(
|
|||||||
const folder = path.join(config.download_path, streamer, dateStr);
|
const folder = path.join(config.download_path, streamer, dateStr);
|
||||||
fs.mkdirSync(folder, { recursive: true });
|
fs.mkdirSync(folder, { recursive: true });
|
||||||
|
|
||||||
const safeTitle = item.title.replace(/[^a-zA-Z0-9_\- ]/g, '').substring(0, 50);
|
|
||||||
const totalDuration = parseDuration(item.duration_str);
|
const totalDuration = parseDuration(item.duration_str);
|
||||||
|
const vodId = parseVodId(item.url);
|
||||||
|
|
||||||
|
const makeTemplateFilename = (
|
||||||
|
template: string,
|
||||||
|
templateFallback: string,
|
||||||
|
partNum: number,
|
||||||
|
trimStartSec: number,
|
||||||
|
trimLengthSec: number
|
||||||
|
): string => {
|
||||||
|
const relativeName = renderClipFilenameTemplate({
|
||||||
|
template: normalizeFilenameTemplate(template, templateFallback),
|
||||||
|
title: item.title,
|
||||||
|
vodId,
|
||||||
|
channel: item.streamer,
|
||||||
|
date,
|
||||||
|
part: partNum,
|
||||||
|
partPadded: partNum.toString().padStart(2, '0'),
|
||||||
|
trimStartSec,
|
||||||
|
trimEndSec: trimStartSec + trimLengthSec,
|
||||||
|
trimLengthSec,
|
||||||
|
fullLengthSec: totalDuration
|
||||||
|
});
|
||||||
|
|
||||||
|
return path.join(folder, relativeName);
|
||||||
|
};
|
||||||
|
|
||||||
// Custom Clip - download specific time range
|
// Custom Clip - download specific time range
|
||||||
if (item.customClip) {
|
if (item.customClip) {
|
||||||
@ -1458,20 +1507,14 @@ async function downloadVOD(
|
|||||||
|
|
||||||
// Helper to generate filename based on format
|
// Helper to generate filename based on format
|
||||||
const makeClipFilename = (partNum: number, startOffset: number, clipLengthSec: number): string => {
|
const makeClipFilename = (partNum: number, startOffset: number, clipLengthSec: number): string => {
|
||||||
if (clip.filenameFormat === 'template' && (clip.filenameTemplate || '').trim()) {
|
if (clip.filenameFormat === 'template') {
|
||||||
const relativeName = renderClipFilenameTemplate({
|
return makeTemplateFilename(
|
||||||
template: clip.filenameTemplate as string,
|
clip.filenameTemplate || config.filename_template_clip,
|
||||||
title: item.title,
|
DEFAULT_FILENAME_TEMPLATE_CLIP,
|
||||||
vodId: parseVodId(item.url),
|
partNum,
|
||||||
channel: item.streamer,
|
startOffset,
|
||||||
date,
|
clipLengthSec
|
||||||
part: partNum,
|
);
|
||||||
trimStartSec: startOffset,
|
|
||||||
trimEndSec: startOffset + clipLengthSec,
|
|
||||||
trimLengthSec: clipLengthSec,
|
|
||||||
fullLengthSec: totalDuration
|
|
||||||
});
|
|
||||||
return path.join(folder, relativeName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clip.filenameFormat === 'timestamp') {
|
if (clip.filenameFormat === 'timestamp') {
|
||||||
@ -1538,7 +1581,13 @@ async function downloadVOD(
|
|||||||
// Check download mode
|
// Check download mode
|
||||||
if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) {
|
if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) {
|
||||||
// Full download
|
// Full download
|
||||||
const filename = path.join(folder, `${safeTitle}.mp4`);
|
const filename = makeTemplateFilename(
|
||||||
|
config.filename_template_vod,
|
||||||
|
DEFAULT_FILENAME_TEMPLATE_VOD,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
totalDuration
|
||||||
|
);
|
||||||
return await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1);
|
return await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1);
|
||||||
} else {
|
} else {
|
||||||
// Part-based download
|
// Part-based download
|
||||||
@ -1553,7 +1602,13 @@ async function downloadVOD(
|
|||||||
const endSec = Math.min((i + 1) * partDuration, totalDuration);
|
const endSec = Math.min((i + 1) * partDuration, totalDuration);
|
||||||
const duration = endSec - startSec;
|
const duration = endSec - startSec;
|
||||||
|
|
||||||
const partFilename = path.join(folder, `${dateStr}_Part${(i + 1).toString().padStart(2, '0')}.mp4`);
|
const partFilename = makeTemplateFilename(
|
||||||
|
config.filename_template_parts,
|
||||||
|
DEFAULT_FILENAME_TEMPLATE_PARTS,
|
||||||
|
i + 1,
|
||||||
|
startSec,
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
|
||||||
const result = await downloadVODPart(
|
const result = await downloadVODPart(
|
||||||
item.url,
|
item.url,
|
||||||
@ -1756,7 +1811,7 @@ ipcMain.handle('save-config', (_, newConfig: Partial<Config>) => {
|
|||||||
const previousClientId = config.client_id;
|
const previousClientId = config.client_id;
|
||||||
const previousClientSecret = config.client_secret;
|
const previousClientSecret = config.client_secret;
|
||||||
|
|
||||||
config = { ...config, ...newConfig };
|
config = normalizeConfigTemplates({ ...config, ...newConfig });
|
||||||
|
|
||||||
if (config.client_id !== previousClientId || config.client_secret !== previousClientSecret) {
|
if (config.client_id !== previousClientId || config.client_secret !== previousClientSecret) {
|
||||||
accessToken = null;
|
accessToken = null;
|
||||||
|
|||||||
3
typescript-version/src/renderer-globals.d.ts
vendored
3
typescript-version/src/renderer-globals.d.ts
vendored
@ -7,6 +7,9 @@ interface AppConfig {
|
|||||||
download_mode?: 'parts' | 'full';
|
download_mode?: 'parts' | 'full';
|
||||||
part_minutes?: number;
|
part_minutes?: number;
|
||||||
language?: 'de' | 'en';
|
language?: 'de' | 'en';
|
||||||
|
filename_template_vod?: string;
|
||||||
|
filename_template_parts?: string;
|
||||||
|
filename_template_clip?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,14 @@ const UI_TEXT_DE = {
|
|||||||
modeFull: 'Ganzes VOD',
|
modeFull: 'Ganzes VOD',
|
||||||
modeParts: 'In Teile splitten',
|
modeParts: 'In Teile splitten',
|
||||||
partMinutesLabel: 'Teil-Lange (Minuten)',
|
partMinutesLabel: 'Teil-Lange (Minuten)',
|
||||||
|
filenameTemplatesTitle: 'Dateinamen-Templates',
|
||||||
|
vodTemplateLabel: 'VOD-Template',
|
||||||
|
partsTemplateLabel: 'VOD-Teile-Template',
|
||||||
|
defaultClipTemplateLabel: 'Clip-Template',
|
||||||
|
filenameTemplateHint: 'Platzhalter: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}',
|
||||||
|
vodTemplatePlaceholder: '{title}.mp4',
|
||||||
|
partsTemplatePlaceholder: '{date}_Part{part_padded}.mp4',
|
||||||
|
defaultClipTemplatePlaceholder: '{date}_{part}.mp4',
|
||||||
updateTitle: 'Updates',
|
updateTitle: 'Updates',
|
||||||
checkUpdates: 'Nach Updates suchen',
|
checkUpdates: 'Nach Updates suchen',
|
||||||
preflightTitle: 'System-Check',
|
preflightTitle: 'System-Check',
|
||||||
@ -122,7 +130,7 @@ const UI_TEXT_DE = {
|
|||||||
formatTemplate: '(benutzerdefiniert)',
|
formatTemplate: '(benutzerdefiniert)',
|
||||||
templateEmpty: 'Das Template darf im benutzerdefinierten Modus nicht leer sein.',
|
templateEmpty: 'Das Template darf im benutzerdefinierten Modus nicht leer sein.',
|
||||||
templatePlaceholder: '{date}_{part}.mp4',
|
templatePlaceholder: '{date}_{part}.mp4',
|
||||||
templateHelp: 'Platzhalter: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
templateHelp: 'Platzhalter: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
||||||
},
|
},
|
||||||
cutter: {
|
cutter: {
|
||||||
videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?',
|
videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?',
|
||||||
|
|||||||
@ -40,6 +40,14 @@ const UI_TEXT_EN = {
|
|||||||
modeFull: 'Full VOD',
|
modeFull: 'Full VOD',
|
||||||
modeParts: 'Split into parts',
|
modeParts: 'Split into parts',
|
||||||
partMinutesLabel: 'Part Length (Minutes)',
|
partMinutesLabel: 'Part Length (Minutes)',
|
||||||
|
filenameTemplatesTitle: 'Filename Templates',
|
||||||
|
vodTemplateLabel: 'VOD Template',
|
||||||
|
partsTemplateLabel: 'VOD Part Template',
|
||||||
|
defaultClipTemplateLabel: 'Clip Template',
|
||||||
|
filenameTemplateHint: 'Placeholders: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}',
|
||||||
|
vodTemplatePlaceholder: '{title}.mp4',
|
||||||
|
partsTemplatePlaceholder: '{date}_Part{part_padded}.mp4',
|
||||||
|
defaultClipTemplatePlaceholder: '{date}_{part}.mp4',
|
||||||
updateTitle: 'Updates',
|
updateTitle: 'Updates',
|
||||||
checkUpdates: 'Check for updates',
|
checkUpdates: 'Check for updates',
|
||||||
preflightTitle: 'System Check',
|
preflightTitle: 'System Check',
|
||||||
@ -122,7 +130,7 @@ const UI_TEXT_EN = {
|
|||||||
formatTemplate: '(custom template)',
|
formatTemplate: '(custom template)',
|
||||||
templateEmpty: 'Template cannot be empty in custom template mode.',
|
templateEmpty: 'Template cannot be empty in custom template mode.',
|
||||||
templatePlaceholder: '{date}_{part}.mp4',
|
templatePlaceholder: '{date}_{part}.mp4',
|
||||||
templateHelp: 'Placeholders: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
templateHelp: 'Placeholders: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
||||||
},
|
},
|
||||||
cutter: {
|
cutter: {
|
||||||
videoInfoFailed: 'Could not read video info. Is FFprobe installed?',
|
videoInfoFailed: 'Could not read video info. Is FFprobe installed?',
|
||||||
|
|||||||
@ -130,15 +130,25 @@ async function saveSettings(): Promise<void> {
|
|||||||
const downloadPath = byId<HTMLInputElement>('downloadPath').value;
|
const downloadPath = byId<HTMLInputElement>('downloadPath').value;
|
||||||
const downloadMode = byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full';
|
const downloadMode = byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full';
|
||||||
const partMinutes = parseInt(byId<HTMLInputElement>('partMinutes').value, 10) || 120;
|
const partMinutes = parseInt(byId<HTMLInputElement>('partMinutes').value, 10) || 120;
|
||||||
|
const vodFilenameTemplate = byId<HTMLInputElement>('vodFilenameTemplate').value.trim() || '{title}.mp4';
|
||||||
|
const partsFilenameTemplate = byId<HTMLInputElement>('partsFilenameTemplate').value.trim() || '{date}_Part{part_padded}.mp4';
|
||||||
|
const defaultClipFilenameTemplate = byId<HTMLInputElement>('defaultClipFilenameTemplate').value.trim() || '{date}_{part}.mp4';
|
||||||
|
|
||||||
config = await window.api.saveConfig({
|
config = await window.api.saveConfig({
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
download_path: downloadPath,
|
download_path: downloadPath,
|
||||||
download_mode: downloadMode,
|
download_mode: downloadMode,
|
||||||
part_minutes: partMinutes
|
part_minutes: partMinutes,
|
||||||
|
filename_template_vod: vodFilenameTemplate,
|
||||||
|
filename_template_parts: partsFilenameTemplate,
|
||||||
|
filename_template_clip: defaultClipFilenameTemplate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
byId<HTMLInputElement>('vodFilenameTemplate').value = (config.filename_template_vod as string) || '{title}.mp4';
|
||||||
|
byId<HTMLInputElement>('partsFilenameTemplate').value = (config.filename_template_parts as string) || '{date}_Part{part_padded}.mp4';
|
||||||
|
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || '{date}_{part}.mp4';
|
||||||
|
|
||||||
await connect();
|
await connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,14 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('modeFullText', UI_TEXT.static.modeFull);
|
setText('modeFullText', UI_TEXT.static.modeFull);
|
||||||
setText('modePartsText', UI_TEXT.static.modeParts);
|
setText('modePartsText', UI_TEXT.static.modeParts);
|
||||||
setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel);
|
setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel);
|
||||||
|
setText('filenameTemplatesTitle', UI_TEXT.static.filenameTemplatesTitle);
|
||||||
|
setText('vodTemplateLabel', UI_TEXT.static.vodTemplateLabel);
|
||||||
|
setText('partsTemplateLabel', UI_TEXT.static.partsTemplateLabel);
|
||||||
|
setText('defaultClipTemplateLabel', UI_TEXT.static.defaultClipTemplateLabel);
|
||||||
|
setText('filenameTemplateHint', UI_TEXT.static.filenameTemplateHint);
|
||||||
|
setPlaceholder('vodFilenameTemplate', UI_TEXT.static.vodTemplatePlaceholder);
|
||||||
|
setPlaceholder('partsFilenameTemplate', UI_TEXT.static.partsTemplatePlaceholder);
|
||||||
|
setPlaceholder('defaultClipFilenameTemplate', UI_TEXT.static.defaultClipTemplatePlaceholder);
|
||||||
setText('updateTitle', UI_TEXT.static.updateTitle);
|
setText('updateTitle', UI_TEXT.static.updateTitle);
|
||||||
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
|
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
|
||||||
setText('preflightTitle', UI_TEXT.static.preflightTitle);
|
setText('preflightTitle', UI_TEXT.static.preflightTitle);
|
||||||
|
|||||||
@ -18,6 +18,9 @@ async function init(): Promise<void> {
|
|||||||
updateLanguagePicker(config.language ?? 'en');
|
updateLanguagePicker(config.language ?? 'en');
|
||||||
byId<HTMLSelectElement>('downloadMode').value = config.download_mode ?? 'full';
|
byId<HTMLSelectElement>('downloadMode').value = config.download_mode ?? 'full';
|
||||||
byId<HTMLInputElement>('partMinutes').value = String(config.part_minutes ?? 120);
|
byId<HTMLInputElement>('partMinutes').value = String(config.part_minutes ?? 120);
|
||||||
|
byId<HTMLInputElement>('vodFilenameTemplate').value = (config.filename_template_vod as string) || DEFAULT_VOD_TEMPLATE;
|
||||||
|
byId<HTMLInputElement>('partsFilenameTemplate').value = (config.filename_template_parts as string) || DEFAULT_PARTS_TEMPLATE;
|
||||||
|
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE;
|
||||||
|
|
||||||
changeTheme(config.theme ?? 'twitch');
|
changeTheme(config.theme ?? 'twitch');
|
||||||
renderStreamers();
|
renderStreamers();
|
||||||
@ -174,6 +177,10 @@ function formatSecondsToTimeDashed(seconds: number): string {
|
|||||||
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')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_VOD_TEMPLATE = '{title}.mp4';
|
||||||
|
const DEFAULT_PARTS_TEMPLATE = '{date}_Part{part_padded}.mp4';
|
||||||
|
const DEFAULT_CLIP_TEMPLATE = '{date}_{part}.mp4';
|
||||||
|
|
||||||
function formatDateWithPattern(date: Date, pattern: string): string {
|
function formatDateWithPattern(date: Date, pattern: string): string {
|
||||||
const tokenMap: Record<string, string> = {
|
const tokenMap: Record<string, string> = {
|
||||||
yyyy: date.getFullYear().toString(),
|
yyyy: date.getFullYear().toString(),
|
||||||
@ -248,6 +255,7 @@ function buildTemplatePreview(template: string, context: {
|
|||||||
.replace(/\{channel_id\}/g, '0')
|
.replace(/\{channel_id\}/g, '0')
|
||||||
.replace(/\{date\}/g, dateStr)
|
.replace(/\{date\}/g, dateStr)
|
||||||
.replace(/\{part\}/g, normalizedPart)
|
.replace(/\{part\}/g, normalizedPart)
|
||||||
|
.replace(/\{part_padded\}/g, normalizedPart.padStart(2, '0'))
|
||||||
.replace(/\{trim_start\}/g, formatSecondsToTimeDashed(context.startSec))
|
.replace(/\{trim_start\}/g, formatSecondsToTimeDashed(context.startSec))
|
||||||
.replace(/\{trim_end\}/g, formatSecondsToTimeDashed(context.startSec + context.durationSec))
|
.replace(/\{trim_end\}/g, formatSecondsToTimeDashed(context.startSec + context.durationSec))
|
||||||
.replace(/\{trim_length\}/g, formatSecondsToTimeDashed(context.durationSec))
|
.replace(/\{trim_length\}/g, formatSecondsToTimeDashed(context.durationSec))
|
||||||
@ -286,7 +294,7 @@ function openClipDialog(url: string, title: string, date: string, streamer: stri
|
|||||||
byId<HTMLInputElement>('clipStartTime').value = '00:00:00';
|
byId<HTMLInputElement>('clipStartTime').value = '00:00:00';
|
||||||
byId<HTMLInputElement>('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds));
|
byId<HTMLInputElement>('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds));
|
||||||
byId<HTMLInputElement>('clipStartPart').value = '';
|
byId<HTMLInputElement>('clipStartPart').value = '';
|
||||||
byId<HTMLInputElement>('clipFilenameTemplate').value = '{date}_{part}.mp4';
|
byId<HTMLInputElement>('clipFilenameTemplate').value = (config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE;
|
||||||
query<HTMLInputElement>('input[name="filenameFormat"][value="simple"]').checked = true;
|
query<HTMLInputElement>('input[name="filenameFormat"][value="simple"]').checked = true;
|
||||||
updateFilenameTemplateVisibility();
|
updateFilenameTemplateVisibility();
|
||||||
|
|
||||||
@ -355,7 +363,7 @@ function updateFilenameExamples(): void {
|
|||||||
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||||
const durationSec = Math.max(1, endSec - startSec);
|
const durationSec = Math.max(1, endSec - startSec);
|
||||||
const timeStr = formatSecondsToTimeDashed(startSec);
|
const timeStr = formatSecondsToTimeDashed(startSec);
|
||||||
const template = byId<HTMLInputElement>('clipFilenameTemplate').value.trim() || '{date}_{part}.mp4';
|
const template = byId<HTMLInputElement>('clipFilenameTemplate').value.trim() || (config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE;
|
||||||
|
|
||||||
updateFilenameTemplateVisibility();
|
updateFilenameTemplateVisibility();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user