586 lines
24 KiB
JavaScript
586 lines
24 KiB
JavaScript
// ==UserScript==
|
|
// @name YouTube: Playlist-Gesamtlänge
|
|
// @namespace https://git.ponywave.de/Akamaru/Userscripts
|
|
// @version 1.0
|
|
// @description Berechnet die Gesamtlänge einer YouTube-Playlist
|
|
// @author Akamaru
|
|
// @match https://www.youtube.com/playlist?*
|
|
// @grant GM_getValue
|
|
// @grant GM_setValue
|
|
// @connect googleapis.com
|
|
// @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/youtube-playlist-duration.user.js
|
|
// @downloadURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/youtube-playlist-duration.user.js
|
|
// ==/UserScript==
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// YouTube API Key (optional) - in localStorage speichern
|
|
const API_KEY_STORAGE = 'youtube_playlist_api_key';
|
|
|
|
// Warte bis die Seite geladen ist
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
function init() {
|
|
// Versuche mehrmals den Button hinzuzufügen
|
|
let attempts = 0;
|
|
const maxAttempts = 10;
|
|
|
|
const tryAddButton = () => {
|
|
attempts++;
|
|
console.log(`[Playlist Duration] Versuch ${attempts}/${maxAttempts}`);
|
|
|
|
if (addCalculateButton()) {
|
|
console.log('[Playlist Duration] Button erfolgreich hinzugefügt');
|
|
return true;
|
|
}
|
|
|
|
if (attempts < maxAttempts) {
|
|
setTimeout(tryAddButton, 1000);
|
|
} else {
|
|
console.error('[Playlist Duration] Konnte Button nicht hinzufügen nach', maxAttempts, 'Versuchen');
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Starte erste Versuche
|
|
setTimeout(tryAddButton, 1000);
|
|
}
|
|
|
|
function addCalculateButton() {
|
|
// Prüfe ob Button bereits existiert
|
|
if (document.getElementById('playlist-duration-button')) {
|
|
console.log('[Playlist Duration] Button existiert bereits');
|
|
return true;
|
|
}
|
|
|
|
// Prüfe ob wir auf einer Playlist-Seite sind
|
|
if (!window.location.href.includes('playlist?list=')) {
|
|
console.log('[Playlist Duration] Nicht auf Playlist-Seite');
|
|
return false;
|
|
}
|
|
|
|
console.log('[Playlist Duration] Erstelle Floating Button');
|
|
|
|
// Erstelle Floating Button (fixiert rechts oben)
|
|
const button = document.createElement('button');
|
|
button.id = 'playlist-duration-button';
|
|
button.textContent = '⏱️ Playlist-Länge berechnen';
|
|
button.style.cssText = `
|
|
position: fixed;
|
|
top: 80px;
|
|
right: 20px;
|
|
z-index: 9998;
|
|
background: #065fd4;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 18px;
|
|
padding: 10px 16px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
transition: background 0.2s;
|
|
`;
|
|
|
|
button.addEventListener('mouseenter', () => {
|
|
button.style.background = '#0656c7';
|
|
});
|
|
|
|
button.addEventListener('mouseleave', () => {
|
|
button.style.background = '#065fd4';
|
|
});
|
|
|
|
button.addEventListener('click', openModal);
|
|
|
|
document.body.appendChild(button);
|
|
|
|
console.log('[Playlist Duration] Button hinzugefügt');
|
|
|
|
return true;
|
|
}
|
|
|
|
function openModal() {
|
|
// Erstelle Modal-Overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'playlist-duration-modal';
|
|
overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;';
|
|
|
|
// Erstelle Modal-Content
|
|
const modal = document.createElement('div');
|
|
modal.style.cssText = 'background: var(--yt-spec-base-background, white); padding: 25px; border-radius: 12px; max-width: 600px; width: 90%; box-shadow: 0 4px 16px rgba(0,0,0,0.3); color: var(--yt-spec-text-primary, black); font-size: 16px;';
|
|
|
|
const title = document.createElement('h2');
|
|
title.textContent = 'Playlist-Gesamtlänge';
|
|
title.style.cssText = 'margin: 0 0 20px 0; font-size: 24px;';
|
|
|
|
const statusText = document.createElement('p');
|
|
statusText.id = 'duration-status';
|
|
statusText.textContent = 'Scrollen und laden der Videos...';
|
|
statusText.style.cssText = 'margin: 15px 0; font-size: 16px;';
|
|
|
|
const infoDiv = document.createElement('div');
|
|
infoDiv.id = 'duration-info';
|
|
infoDiv.style.cssText = 'margin: 20px 0; display: none;';
|
|
|
|
const wikiDiv = document.createElement('div');
|
|
wikiDiv.id = 'wiki-format';
|
|
wikiDiv.style.cssText = 'margin: 20px 0; display: none;';
|
|
|
|
// API Key Eingabe (ausklappbar)
|
|
const apiKeySection = document.createElement('div');
|
|
apiKeySection.style.cssText = 'margin: 15px 0; padding: 15px; background: rgba(0,0,0,0.05); border-radius: 8px;';
|
|
|
|
const apiKeyHeader = document.createElement('div');
|
|
apiKeyHeader.style.cssText = 'display: flex; align-items: center; justify-content: space-between; cursor: pointer;';
|
|
apiKeyHeader.innerHTML = '<span style="font-weight: bold;">⚙️ YouTube API Key (optional)</span><span id="api-toggle">▼</span>';
|
|
|
|
const apiKeyContent = document.createElement('div');
|
|
apiKeyContent.id = 'api-key-content';
|
|
apiKeyContent.style.cssText = 'display: none; margin-top: 10px;';
|
|
|
|
const apiKeyInfo = document.createElement('div');
|
|
apiKeyInfo.style.cssText = 'font-size: 14px; color: #666; margin-bottom: 8px;';
|
|
apiKeyInfo.textContent = 'Mit einem API-Key werden die exakten Upload-Daten automatisch abgerufen.';
|
|
|
|
const apiKeyInputDiv = document.createElement('div');
|
|
apiKeyInputDiv.style.cssText = 'display: flex; gap: 8px; margin-bottom: 8px;';
|
|
|
|
const apiKeyInput = document.createElement('input');
|
|
apiKeyInput.type = 'password';
|
|
apiKeyInput.id = 'api-key-input';
|
|
apiKeyInput.placeholder = 'API-Key eingeben...';
|
|
apiKeyInput.style.cssText = 'flex: 1; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace;';
|
|
apiKeyInput.value = GM_getValue(API_KEY_STORAGE, '');
|
|
|
|
const toggleVisibilityBtn = document.createElement('button');
|
|
toggleVisibilityBtn.textContent = '👁️';
|
|
toggleVisibilityBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; background: white;';
|
|
toggleVisibilityBtn.onclick = (e) => {
|
|
e.preventDefault();
|
|
apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password';
|
|
};
|
|
|
|
const saveApiKeyBtn = document.createElement('button');
|
|
saveApiKeyBtn.textContent = 'Speichern';
|
|
saveApiKeyBtn.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--filled yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
|
|
saveApiKeyBtn.style.cssText = 'padding: 6px 12px;';
|
|
saveApiKeyBtn.onclick = (e) => {
|
|
e.preventDefault();
|
|
const key = apiKeyInput.value.trim();
|
|
if (key) {
|
|
GM_setValue(API_KEY_STORAGE, key);
|
|
alert('API-Key gespeichert!');
|
|
} else {
|
|
GM_setValue(API_KEY_STORAGE, '');
|
|
alert('API-Key gelöscht!');
|
|
}
|
|
};
|
|
|
|
apiKeyInputDiv.appendChild(apiKeyInput);
|
|
apiKeyInputDiv.appendChild(toggleVisibilityBtn);
|
|
apiKeyInputDiv.appendChild(saveApiKeyBtn);
|
|
|
|
const apiKeyHelp = document.createElement('div');
|
|
apiKeyHelp.style.cssText = 'font-size: 13px; color: #999; margin-top: 5px;';
|
|
apiKeyHelp.innerHTML = 'Wie erstelle ich einen API-Key? <a href="#" id="api-help-link" style="color: #065fd4;">Anleitung anzeigen</a>';
|
|
|
|
apiKeyContent.appendChild(apiKeyInfo);
|
|
apiKeyContent.appendChild(apiKeyInputDiv);
|
|
apiKeyContent.appendChild(apiKeyHelp);
|
|
|
|
apiKeySection.appendChild(apiKeyHeader);
|
|
apiKeySection.appendChild(apiKeyContent);
|
|
|
|
// Toggle für API-Key Sektion
|
|
apiKeyHeader.onclick = () => {
|
|
const isOpen = apiKeyContent.style.display === 'block';
|
|
apiKeyContent.style.display = isOpen ? 'none' : 'block';
|
|
document.getElementById('api-toggle').textContent = isOpen ? '▼' : '▲';
|
|
};
|
|
|
|
// Datum-Eingabefelder
|
|
const dateInputsDiv = document.createElement('div');
|
|
dateInputsDiv.style.cssText = 'margin: 15px 0; padding: 15px; background: rgba(0,0,0,0.05); border-radius: 8px;';
|
|
|
|
const dateTitle = document.createElement('div');
|
|
dateTitle.textContent = 'Zeitraum:';
|
|
dateTitle.style.cssText = 'font-weight: bold; margin-bottom: 10px;';
|
|
|
|
const startDateDiv = document.createElement('div');
|
|
startDateDiv.style.cssText = 'margin: 8px 0;';
|
|
const startDateLabel = document.createElement('label');
|
|
startDateLabel.textContent = 'Start-Datum: ';
|
|
startDateLabel.style.marginRight = '8px';
|
|
const startDateInput = document.createElement('input');
|
|
startDateInput.type = 'date';
|
|
startDateInput.id = 'start-date-input';
|
|
startDateInput.style.cssText = 'padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px;';
|
|
startDateDiv.appendChild(startDateLabel);
|
|
startDateDiv.appendChild(startDateInput);
|
|
|
|
const endDateDiv = document.createElement('div');
|
|
endDateDiv.style.cssText = 'margin: 8px 0;';
|
|
const endDateLabel = document.createElement('label');
|
|
endDateLabel.textContent = 'End-Datum: ';
|
|
endDateLabel.style.marginRight = '8px';
|
|
const endDateInput = document.createElement('input');
|
|
endDateInput.type = 'date';
|
|
endDateInput.id = 'end-date-input';
|
|
endDateInput.style.cssText = 'padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px;';
|
|
endDateDiv.appendChild(endDateLabel);
|
|
endDateDiv.appendChild(endDateInput);
|
|
|
|
dateInputsDiv.appendChild(dateTitle);
|
|
dateInputsDiv.appendChild(startDateDiv);
|
|
dateInputsDiv.appendChild(endDateDiv);
|
|
|
|
const wikiCheckboxDiv = document.createElement('div');
|
|
wikiCheckboxDiv.style.cssText = 'margin: 10px 0;';
|
|
|
|
const finishedCheckbox = document.createElement('input');
|
|
finishedCheckbox.type = 'checkbox';
|
|
finishedCheckbox.id = 'series-finished';
|
|
finishedCheckbox.style.marginRight = '8px';
|
|
|
|
const finishedLabel = document.createElement('label');
|
|
finishedLabel.htmlFor = 'series-finished';
|
|
finishedLabel.textContent = 'Serie beendet';
|
|
|
|
wikiCheckboxDiv.appendChild(finishedCheckbox);
|
|
wikiCheckboxDiv.appendChild(finishedLabel);
|
|
|
|
const wikiTextarea = document.createElement('textarea');
|
|
wikiTextarea.id = 'wiki-output';
|
|
wikiTextarea.readOnly = true;
|
|
wikiTextarea.style.cssText = 'width: 100%; height: 150px; font-family: monospace; font-size: 14px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; resize: vertical;';
|
|
|
|
const copyBtn = document.createElement('button');
|
|
copyBtn.textContent = 'Wiki-Format kopieren';
|
|
copyBtn.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--filled yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
|
|
copyBtn.style.cssText = 'margin-top: 10px; margin-right: 10px;';
|
|
copyBtn.onclick = () => {
|
|
wikiTextarea.select();
|
|
document.execCommand('copy');
|
|
copyBtn.textContent = 'Kopiert!';
|
|
setTimeout(() => copyBtn.textContent = 'Wiki-Format kopieren', 2000);
|
|
};
|
|
|
|
wikiDiv.appendChild(apiKeySection);
|
|
wikiDiv.appendChild(dateInputsDiv);
|
|
wikiDiv.appendChild(wikiCheckboxDiv);
|
|
wikiDiv.appendChild(wikiTextarea);
|
|
wikiDiv.appendChild(copyBtn);
|
|
|
|
// API-Help Link Handler
|
|
setTimeout(() => {
|
|
document.getElementById('api-help-link')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
showApiHelp();
|
|
});
|
|
}, 100);
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.textContent = 'Schließen';
|
|
closeBtn.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
|
|
closeBtn.style.cssText = 'margin-top: 20px;';
|
|
closeBtn.onclick = () => overlay.remove();
|
|
|
|
modal.appendChild(title);
|
|
modal.appendChild(statusText);
|
|
modal.appendChild(infoDiv);
|
|
modal.appendChild(wikiDiv);
|
|
modal.appendChild(closeBtn);
|
|
overlay.appendChild(modal);
|
|
document.body.appendChild(overlay);
|
|
|
|
// Schließe Modal bei Klick auf Overlay
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) overlay.remove();
|
|
});
|
|
|
|
// Starte Berechnung
|
|
calculateDuration(finishedCheckbox, startDateInput, endDateInput);
|
|
}
|
|
|
|
async function calculateDuration(finishedCheckbox, startDateInput, endDateInput) {
|
|
const statusText = document.getElementById('duration-status');
|
|
const infoDiv = document.getElementById('duration-info');
|
|
const wikiDiv = document.getElementById('wiki-format');
|
|
const wikiOutput = document.getElementById('wiki-output');
|
|
|
|
// Automatisches Scrollen bis alle Videos geladen sind
|
|
statusText.textContent = 'Scrolle bis alle Videos geladen sind...';
|
|
await autoScrollToEnd();
|
|
|
|
statusText.textContent = 'Berechne Gesamtlänge...';
|
|
|
|
// Sammle alle Video-IDs und Dauern
|
|
const videoData = new Map(); // Map um Duplikate zu vermeiden
|
|
const videoElements = document.querySelectorAll('ytd-playlist-video-renderer');
|
|
|
|
videoElements.forEach(element => {
|
|
// Extrahiere Video-ID
|
|
const videoLink = element.querySelector('a#video-title');
|
|
if (!videoLink) return;
|
|
|
|
const videoId = new URLSearchParams(videoLink.href.split('?')[1]).get('v');
|
|
if (!videoId) return;
|
|
|
|
// Extrahiere Dauer
|
|
const durationElement = element.querySelector('ytd-thumbnail-overlay-time-status-renderer span');
|
|
if (!durationElement) return;
|
|
|
|
const durationText = durationElement.textContent.trim();
|
|
if (!durationText) return;
|
|
|
|
// Speichere nur einzigartige Videos
|
|
if (!videoData.has(videoId)) {
|
|
videoData.set(videoId, durationText);
|
|
}
|
|
});
|
|
|
|
// Versuche Daten via API zu holen, falls API-Key vorhanden
|
|
const apiKey = GM_getValue(API_KEY_STORAGE, '');
|
|
let oldestDate = null;
|
|
let newestDate = null;
|
|
|
|
if (apiKey) {
|
|
statusText.textContent = 'Lade Upload-Daten von YouTube API...';
|
|
const playlistId = new URLSearchParams(window.location.search).get('list');
|
|
const dates = await fetchPlaylistDates(playlistId, apiKey);
|
|
|
|
if (dates && dates.length > 0) {
|
|
dates.sort((a, b) => a - b);
|
|
oldestDate = dates[0];
|
|
newestDate = dates[dates.length - 1];
|
|
console.log('[Playlist Duration] API-Daten erfolgreich geladen:', { oldestDate, newestDate });
|
|
} else {
|
|
console.log('[Playlist Duration] API-Aufruf fehlgeschlagen oder keine Daten');
|
|
}
|
|
}
|
|
|
|
// Berechne Gesamtdauer
|
|
let totalMinutes = 0;
|
|
videoData.forEach((durationText) => {
|
|
const parts = durationText.split(':').map(Number);
|
|
let seconds = 0;
|
|
if (parts.length === 3) {
|
|
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
} else if (parts.length === 2) {
|
|
seconds = parts[0] * 60 + parts[1];
|
|
}
|
|
totalMinutes += Math.ceil(seconds / 60); // Aufrunden auf volle Minuten
|
|
});
|
|
|
|
const videoCount = videoData.size;
|
|
|
|
// Zeige Informationen
|
|
statusText.style.display = 'none';
|
|
infoDiv.style.display = 'block';
|
|
infoDiv.innerHTML = `
|
|
<p style="margin: 5px 0;"><strong>Videos:</strong> ${videoCount} Folgen</p>
|
|
<p style="margin: 5px 0;"><strong>Gesamtlänge:</strong> ~${totalMinutes} Minuten</p>
|
|
`;
|
|
|
|
// Setze Daten: API-Daten falls vorhanden, sonst heutiges Datum
|
|
if (oldestDate && newestDate) {
|
|
startDateInput.valueAsDate = oldestDate;
|
|
endDateInput.valueAsDate = newestDate;
|
|
} else {
|
|
const today = new Date();
|
|
startDateInput.valueAsDate = today;
|
|
endDateInput.valueAsDate = today;
|
|
}
|
|
|
|
// Generiere Wiki-Format
|
|
wikiDiv.style.display = 'block';
|
|
const updateWiki = () => {
|
|
generateWikiFormat(videoCount, totalMinutes, finishedCheckbox, wikiOutput, startDateInput, endDateInput);
|
|
};
|
|
updateWiki();
|
|
|
|
// Update Wiki-Format bei Änderungen
|
|
finishedCheckbox.addEventListener('change', updateWiki);
|
|
startDateInput.addEventListener('change', updateWiki);
|
|
endDateInput.addEventListener('change', updateWiki);
|
|
}
|
|
|
|
function generateWikiFormat(videoCount, totalMinutes, finishedCheckbox, wikiOutput, startDateInput, endDateInput) {
|
|
const playlistUrl = window.location.href;
|
|
|
|
// Verwende eingegebene Daten
|
|
const startDate = startDateInput.value ? parseDateInput(startDateInput.value) : formatDate(new Date());
|
|
const endDate = finishedCheckbox.checked ?
|
|
(endDateInput.value ? parseDateInput(endDateInput.value) : formatDate(new Date())) :
|
|
null;
|
|
|
|
const finishedClass = finishedCheckbox.checked ? ' lp-finished' : '';
|
|
|
|
// Wiki-Text erstellen
|
|
let wikiLines = [];
|
|
wikiLines.push(`|class="dates"| {{SortKey|${startDate.iso}|${startDate.display}}}`);
|
|
|
|
// Nur End-Datum Zeile hinzufügen, wenn Serie beendet
|
|
if (finishedCheckbox.checked && endDate) {
|
|
wikiLines.push(`|class="dates${finishedClass}"| {{SortKey|${endDate.iso}|${endDate.display}}}`);
|
|
}
|
|
|
|
wikiLines.push(`|class="episodes"| {{nts|${videoCount}}} Folgen`);
|
|
wikiLines.push(`|class="length"| ~ {{nts|${totalMinutes}}} Minuten`);
|
|
wikiLines.push(`|class="link"| <div align="center">[[Datei:YouTubeIcon.png|link=${playlistUrl}]]</div>`);
|
|
|
|
wikiOutput.value = wikiLines.join('\n');
|
|
}
|
|
|
|
function parseDateInput(dateString) {
|
|
// Date Input Format: YYYY-MM-DD
|
|
const date = new Date(dateString);
|
|
return formatDate(date);
|
|
}
|
|
|
|
function formatDate(date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return {
|
|
iso: `${year}-${month}-${day}`,
|
|
display: `${day}.${month}.${year}`
|
|
};
|
|
}
|
|
|
|
async function autoScrollToEnd() {
|
|
let lastHeight = 0;
|
|
let sameHeightCount = 0;
|
|
|
|
while (sameHeightCount < 3) { // Warte auf 3 gleiche Höhen um sicher zu sein
|
|
window.scrollTo(0, document.documentElement.scrollHeight);
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
const newHeight = document.documentElement.scrollHeight;
|
|
if (newHeight === lastHeight) {
|
|
sameHeightCount++;
|
|
} else {
|
|
sameHeightCount = 0;
|
|
}
|
|
lastHeight = newHeight;
|
|
}
|
|
|
|
// Scrolle zurück nach oben
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
async function fetchPlaylistDates(playlistId, apiKey) {
|
|
try {
|
|
const dates = [];
|
|
let pageToken = '';
|
|
const maxResults = 50;
|
|
|
|
// Hole alle Videos der Playlist (paginiert)
|
|
do {
|
|
const url = `https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails&playlistId=${playlistId}&maxResults=${maxResults}&pageToken=${pageToken}&key=${apiKey}`;
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
console.error('[Playlist Duration] API Error:', response.status, response.statusText);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Sammle Video-IDs
|
|
const videoIds = data.items.map(item => item.contentDetails.videoId);
|
|
|
|
// Hole Video-Details (inkl. Upload-Datum)
|
|
const videoDetailsUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoIds.join(',')}&key=${apiKey}`;
|
|
const videoResponse = await fetch(videoDetailsUrl);
|
|
|
|
if (!videoResponse.ok) {
|
|
console.error('[Playlist Duration] Video API Error:', videoResponse.status);
|
|
return null;
|
|
}
|
|
|
|
const videoData = await videoResponse.json();
|
|
|
|
// Extrahiere Upload-Daten
|
|
videoData.items.forEach(video => {
|
|
if (video.snippet && video.snippet.publishedAt) {
|
|
dates.push(new Date(video.snippet.publishedAt));
|
|
}
|
|
});
|
|
|
|
pageToken = data.nextPageToken || '';
|
|
} while (pageToken);
|
|
|
|
return dates;
|
|
} catch (error) {
|
|
console.error('[Playlist Duration] Fehler beim API-Aufruf:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function showApiHelp() {
|
|
const helpOverlay = document.createElement('div');
|
|
helpOverlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 10000; display: flex; align-items: center; justify-content: center;';
|
|
|
|
const helpModal = document.createElement('div');
|
|
helpModal.style.cssText = 'background: white; padding: 30px; border-radius: 12px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; color: black;';
|
|
|
|
helpModal.innerHTML = `
|
|
<h2 style="margin-top: 0;">YouTube API Key erstellen</h2>
|
|
|
|
<h3>1. Google Cloud Console öffnen</h3>
|
|
<p>Gehe zu: <a href="https://console.cloud.google.com/" target="_blank" style="color: #065fd4;">https://console.cloud.google.com/</a></p>
|
|
|
|
<h3>2. Neues Projekt erstellen</h3>
|
|
<ul>
|
|
<li>Klicke oben auf "Projekt auswählen" → "Neues Projekt"</li>
|
|
<li>Name: z.B. "YouTube Playlist Duration"</li>
|
|
<li>Klicke auf "Erstellen"</li>
|
|
</ul>
|
|
|
|
<h3>3. YouTube Data API v3 aktivieren</h3>
|
|
<ul>
|
|
<li>Gehe zu "APIs & Dienste" → "Bibliothek"</li>
|
|
<li>Suche nach "YouTube Data API v3"</li>
|
|
<li>Klicke auf "Aktivieren"</li>
|
|
</ul>
|
|
|
|
<h3>4. API-Key erstellen</h3>
|
|
<ul>
|
|
<li>Gehe zu "APIs & Dienste" → "Anmeldedaten"</li>
|
|
<li>Klicke auf "+ Anmeldedaten erstellen" → "API-Schlüssel"</li>
|
|
<li>Kopiere den generierten Key</li>
|
|
</ul>
|
|
|
|
<h3>5. API-Key einschränken (empfohlen)</h3>
|
|
<ul>
|
|
<li>Klicke auf den erstellten Key</li>
|
|
<li>Bei "API-Einschränkungen": Wähle "Schlüssel einschränken"</li>
|
|
<li>Aktiviere nur "YouTube Data API v3"</li>
|
|
<li>Speichern</li>
|
|
</ul>
|
|
|
|
<p style="margin-top: 20px; padding: 10px; background: #f0f0f0; border-radius: 5px; font-size: 12px;">
|
|
<strong>Hinweis:</strong> Die YouTube API hat ein kostenloses Kontingent von 10.000 Einheiten/Tag.
|
|
Eine Playlist-Abfrage kostet ca. 1-3 Einheiten pro 50 Videos.
|
|
</p>
|
|
|
|
<button id="close-help" style="margin-top: 20px; padding: 10px 20px; background: #065fd4; color: white; border: none; border-radius: 5px; cursor: pointer;">Schließen</button>
|
|
`;
|
|
|
|
helpOverlay.appendChild(helpModal);
|
|
document.body.appendChild(helpOverlay);
|
|
|
|
helpOverlay.addEventListener('click', (e) => {
|
|
if (e.target === helpOverlay || e.target.id === 'close-help') {
|
|
helpOverlay.remove();
|
|
}
|
|
});
|
|
}
|
|
})();
|