Last active
March 28, 2025 14:02
-
-
Save Kurotaku-sama/a9a91a72b74fda964f6e95e90526caae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Podcast.de Autodownload | |
// @namespace https://kurotaku.de | |
// @version 1.0 | |
// @description Ermöglicht das automatische Herunterladen von Podcast.de-Episoden mit benutzerdefinierten Dateinamen-Templates | |
// @author Kurotaku | |
// @license CC BY-NC-SA 4.0 | |
// @include https://*.podcast.de* | |
// @icon https://www.podcast.de/images/icons/maskable_icon.png | |
// @updateURL https://gist.github.com/Kurotaku-sama/a9a91a72b74fda964f6e95e90526caae/raw/Podcast.de%2520Autodownload.user.js | |
// @downloadURL https://gist.github.com/Kurotaku-sama/a9a91a72b74fda964f6e95e90526caae/raw/Podcast.de%2520Autodownload.user.js | |
// @require https://gist.github.com/Kurotaku-sama/1a7dcb227ce3d7a1b596afe725c0052a/raw/kuros_library.js | |
// @require https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js | |
// @require https://cdn.jsdelivr.net/npm/sweetalert2 | |
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_addStyle | |
// @grant GM_registerMenuCommand | |
// ==/UserScript== | |
(async function() { | |
await init_gm_config(); | |
if (GM_config.get("enable_template_tester")) | |
GM_registerMenuCommand("Template Tester", template_tester); | |
if (GM_config.get("auto_download_enabled") && window.location.pathname.includes("/episode/")) { | |
if (episode && episode.length > 0) { | |
let url = episode[0].url.split("?")[0]; | |
fetch_metadata_and_download(url); | |
} else | |
console.error("Episode URL not found."); | |
} | |
})(); | |
function init_gm_config() { | |
const summary = `<details> | |
<summary>Info: (klick mich)</summary> | |
<ul> | |
<li>- Verfügbare Tags: Alle Tags aus den Metadaten, z. B. {album}, {track}, {title}, {artist}, etc.</li> | |
<li>- Separatoren: | |
<ul> | |
<li>{sep_v:"-"} fügt eine beliebige Zeichenkette ein, wenn das <b>vorherige</b> Tag existiert und nicht leer ist.</li> | |
<li>{sep_n:"-"} fügt eine beliebige Zeichenkette ein, wenn das <b>nächste</b> Tag existiert und nicht leer ist.</li> | |
<li>{sep_mv:"-"} fügt eine beliebige Zeichenkette ein, wenn <b>irgendein vorheriges Tag</b> (bis zum Anfang oder vorherigen Separator) existiert und nicht leer ist.</li> | |
<li>{sep_mn:"-"} fügt eine beliebige Zeichenkette ein, wenn <b>irgendein nachfolgendes Tag</b> (bis zum Ende oder nächsten Separator) existiert und nicht leer ist.</li> | |
</ul> | |
</li> | |
<li>- Beispiel: "{album} {sep_v:'-'} {title} {sep_n:'['}{track}{sep_n:']'}"</li> | |
<li>- Bei Separatoren können " oder ' verwendet werden</li> | |
<li>- Doppelte Leerzeichen werden automatisch entfernt.</li> | |
<li>- Sonderzeichen in Windows werden durch ähnliche ersetzt.</li> | |
</ul> | |
</details>`; | |
GM_registerMenuCommand("Einstellungen", () => GM_config.open()); | |
GM_config.init( | |
{ | |
"id": "configuration", | |
"title": "Podcast.de Autodownload", | |
"fields": | |
{ | |
"auto_download_enabled": { "type": "checkbox", "default": true, "label": "Automatischen Download aktivieren/deaktivieren" }, | |
"enable_template_tester": { "type": "checkbox", "default": true, "label": "Template-Tester aktivieren" }, | |
"auto_download_delay": { "type": "int", "min": 0, "max": 300, "default": 3, "label": "Verzögerung vor dem Download (in Sekunden)" }, | |
"file_name_template": { "type": "input", "default": "{album} {sep_v:'-'} {track} {sep_v:'-'} {title}", "label": `Dateinamen-Template ${summary}` } | |
}, | |
'events': { | |
'save': () => {location.reload()}, | |
}, | |
"frame": document.body.appendChild(document.createElement("div")), | |
} | |
); | |
} | |
function get_episode_name() { | |
let title_element = document.querySelector(".title > h1"); | |
return title_element?.innerText.trim(); | |
} | |
function sanitize_filename(filename) { | |
let replacements = { | |
":": "꞉", "?": "?", "/": "⧸", "\\": "⧹", "|": "|", | |
"\"": """, "*": "*", "<": "<", ">": ">" | |
}; | |
return filename.replace(/[<>:"/\\|?*]/g, char => replacements[char] || char); | |
} | |
async function fetch_metadata_and_download(url) { | |
try { | |
let response = await fetch(url); | |
let blob = await response.blob(); | |
let array_buffer = await blob.arrayBuffer(); | |
let alternate_name = get_episode_name(); | |
jsmediatags.read(new Blob([array_buffer]), { | |
onSuccess: function(tag) { | |
let metadata = tag.tags; | |
metadata.title = metadata.title || alternate_name; | |
let filename = build_filename_from_template(GM_config.get("file_name_template"), metadata); | |
download_file(blob, filename); | |
}, | |
onError: function(error) { | |
download_file(blob, sanitize_filename(alternate_name + ".mp3")); | |
} | |
}); | |
} catch (error) { | |
console.error("Download failed:", error); | |
} | |
} | |
function build_filename_from_template(template, metadata) { | |
let fields = template.match(/{([^{}]+)}/g)?.map(match => match.slice(1, -1)) || []; | |
let result = template; | |
let previous_tag_exists = false; | |
for (let i = 0; i < fields.length; i++) { | |
let temp = fields[i]; | |
switch (true) { | |
case temp.startsWith("sep_v:"): | |
if (i !== 0 && previous_tag_exists) { | |
let separator = temp.split(":")[1].slice(1, -1); | |
result = result.replace(`{${temp}}`, separator); | |
previous_tag_exists = false; | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
break; | |
case temp.startsWith("sep_mv:"): | |
if (i !== 0) { | |
let has_previous_tag = false; | |
for (let j = i - 1; j >= 0; j--) { | |
let prev_temp = fields[j]; | |
if (prev_temp.startsWith("sep_")) break; | |
if (metadata[prev_temp] && metadata[prev_temp].trim() !== "") { | |
has_previous_tag = true; | |
break; | |
} | |
} | |
if (has_previous_tag) { | |
let separator = temp.split(":")[1].slice(1, -1); | |
result = result.replace(`{${temp}}`, separator); | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
break; | |
case temp.startsWith("sep_n:"): | |
if (i !== fields.length - 1) { | |
let next_temp = fields[i + 1]; | |
if (next_temp && metadata[next_temp] && metadata[next_temp].trim() !== "") { | |
let separator = temp.split(":")[1].slice(1, -1); | |
result = result.replace(`{${temp}}`, separator); | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
break; | |
case temp.startsWith("sep_mn:"): | |
if (i !== fields.length - 1) { | |
let has_next_tag = false; | |
for (let j = i + 1; j < fields.length; j++) { | |
let next_temp = fields[j]; | |
if (next_temp.startsWith("sep_")) break; | |
if (metadata[next_temp] && metadata[next_temp].trim() !== "") { | |
has_next_tag = true; | |
break; | |
} | |
} | |
if (has_next_tag) { | |
let separator = temp.split(":")[1].slice(1, -1); | |
result = result.replace(`{${temp}}`, separator); | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
} else | |
result = result.replace(`{${temp}}`, ""); | |
break; | |
default: | |
if (metadata[temp] && metadata[temp].trim() !== "") { | |
result = result.replace(`{${temp}}`, metadata[temp]); | |
previous_tag_exists = true; | |
} else { | |
result = result.replace(`{${temp}}`, ""); | |
previous_tag_exists = false; | |
} | |
break; | |
} | |
} | |
result = result.replace(/\s+/g, " ").trim(); | |
return sanitize_filename(result) + ".mp3"; | |
} | |
async function download_file(blob, filename) { | |
await sleep_s(GM_config.get("auto_download_delay")); | |
let html = `<a href="${URL.createObjectURL(blob)}" download="${filename}" id="temp_download_link"></a>`; | |
document.body.insertAdjacentHTML("beforeend", html); | |
document.getElementById("temp_download_link").click(); | |
document.getElementById("temp_download_link").remove(); | |
Swal.fire({ | |
title: "Download gestartet", | |
text: `Die Datei "${filename}" wird heruntergeladen.`, | |
icon: "success", | |
confirmButtonText: "OK", | |
theme: "dark" | |
}); | |
} | |
function template_tester() { | |
let test_metadata = { | |
album: "Album-Name", | |
track: "1", | |
title: "Title-Name", | |
artist: "Artist-Name", | |
year: "2025", | |
genre: "Podcast" | |
}; | |
Swal.fire({ | |
title: "Template Tester", | |
html: ` | |
<div style="text-align: left; margin-bottom: 10px;"> | |
<strong>Available sample Metadata Tags:</strong> | |
<ul style="list-style-type: none; padding-left: 0;"> | |
${Object.keys(test_metadata).map(key => `<li><code>{${key}}</code>: ${test_metadata[key]}</li>`).join("")} | |
</ul> | |
</div> | |
<input id="template_input" class="swal2-input" style="width: 100%; margin: unset" placeholder="Enter your template"value="${GM_config.get('file_name_template')}"> | |
<div id="template_result" style="margin-top: 10px; color: #fff; font-size: 14px;"></div> | |
`, | |
showConfirmButton: false, // Confirm-Button entfernt | |
showCancelButton: true, | |
cancelButtonText: "Close", | |
theme: "dark", | |
didOpen: () => { | |
const input = document.getElementById("template_input"); | |
const result = document.getElementById("template_result"); | |
input.addEventListener("input", () => { | |
let template = input.value.trim(); | |
if (template) { | |
let filename = build_filename_from_template(template, test_metadata); | |
result.textContent = `Result: ${filename}`; | |
} else | |
result.textContent = ""; | |
}); | |
} | |
}); | |
} | |
GM_addStyle(` | |
#configuration { | |
padding: 20px !important; | |
height: auto !important; | |
max-height: 600px !important; | |
max-width: 500px !important; | |
background: inherit !important; | |
} | |
#configuration input { | |
margin-right: 10px; | |
max-width:40px; | |
} | |
#configuration #configuration_field_file_name_template { | |
width: 100%; | |
max-width: 100%; | |
} | |
#configuration #configuration_resetLink { | |
color: var(--swiper-theme-color); | |
} | |
`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment