Skip to content

Instantly share code, notes, and snippets.

@Kurotaku-sama
Last active March 28, 2025 14:02
Show Gist options
  • Save Kurotaku-sama/a9a91a72b74fda964f6e95e90526caae to your computer and use it in GitHub Desktop.
Save Kurotaku-sama/a9a91a72b74fda964f6e95e90526caae to your computer and use it in GitHub Desktop.
// ==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