Play button + autoplay on press

This commit is contained in:
2025-12-13 08:35:54 +01:00
parent 14e417d72a
commit 9ce74da4ec
3 changed files with 467 additions and 336 deletions

View File

@@ -12,7 +12,7 @@ serving
this scraper will download the gif and mp3 from a ytmnd and write a file embedding these things in addition to zoom text (if any).
The downloaded files cannot be loaded from a `file://` url. In order to view these files, put them online or run a local server. For example, `python -m http.server` from the directory and got to [http://localhost:8000/](http://localhost:8000/). If you host them somewhere, remember to include `ytmnd.js` in the same directory.
The downloaded files cannot be loaded from a `file://` url. In order to view these files, put them online or run a local server. For example, `python -m http.server` from the directory and got to [http://localhost:8000/](http://localhost:8000/).
options
-------

View File

@@ -1,43 +0,0 @@
(function () {
var audio = new Audio(url);
audio.loop = true;
audio.muted = true;
audio
.play()
.then(function () {
console.log("Audio started (muted). Click/tap to unmute!");
var unmuteMsg = document.createElement("div");
unmuteMsg.textContent = "Click to unmute";
unmuteMsg.style.cssText =
"position:fixed;top:10px;right:10px;background:rgba(0,0,0,0.8);color:#fff;padding:10px 20px;border-radius:5px;font-family:sans-serif;z-index:9999;cursor:pointer;";
document.body.appendChild(unmuteMsg);
function unmute() {
audio.muted = false;
unmuteMsg.remove();
console.log("Audio unmuted!");
}
document.addEventListener("click", unmute, { once: true });
document.addEventListener("keydown", unmute, { once: true });
document.addEventListener("touchstart", unmute, { once: true });
unmuteMsg.addEventListener("click", unmute, { once: true });
})
.catch(function (error) {
console.error("Autoplay failed even when muted:", error);
function playOnInteraction() {
audio.muted = false;
audio
.play()
.then(function () {
console.log("Audio started after user interaction");
})
.catch(function (err) {
console.error("Still couldn't play:", err);
});
}
document.addEventListener("click", playOnInteraction, { once: true });
document.addEventListener("keydown", playOnInteraction, { once: true });
document.addEventListener("touchstart", playOnInteraction, {
once: true,
});
});
})();

368
ytmndd.py
View File

@@ -1,18 +1,19 @@
#!/usr/bin/env python3
import sys
import json
import os
import os.path
import re
import time
import json
import subprocess
import sys
import time
from optparse import OptionParser
import requests
from requests.exceptions import RequestException
class YTMND:
class YTMND:
def __init__(self):
self.user_mode = False
self.media_only = False
@@ -29,8 +30,10 @@ class YTMND:
ytmnd_name = user
try:
response = requests.get("http://ytmnd.com/users/" + ytmnd_name + "/sites",
headers={'User-Agent': 'Mozilla/5.0'})
response = requests.get(
"http://ytmnd.com/users/" + ytmnd_name + "/sites",
headers={"User-Agent": "Mozilla/5.0"},
)
response.raise_for_status()
ytmnd_html = response.text.splitlines()
except RequestException as e:
@@ -40,7 +43,7 @@ class YTMND:
domains = []
for line in ytmnd_html:
if 'profile_link' in line:
if "profile_link" in line:
expr = r"site_link\" href=\"http://(\S+).ytmn(d|sfw)?.com\""
match = re.search(expr, line)
if match:
@@ -64,14 +67,11 @@ class YTMND:
print(">> found %d domains" % len(domains))
os.makedirs(user, exist_ok=True)
os.chdir(user)
if not self.no_web_audio:
self.copy_ytmnd_js()
for domain in domains:
self.fetch_ytmnd(domain)
os.chdir("..")
def fetch_ytmnd(self, domain):
if domain == "":
print("expecting one ytmnd name, got " + str(sys.argv))
return None
@@ -83,8 +83,9 @@ class YTMND:
ytmnd_name = domain
try:
response = requests.get("http://" + domain + ".ytmnd.com",
headers={'User-Agent': 'Mozilla/5.0'})
response = requests.get(
"http://" + domain + ".ytmnd.com", headers={"User-Agent": "Mozilla/5.0"}
)
response.raise_for_status()
ytmnd_html = response.text
@@ -95,8 +96,10 @@ class YTMND:
return None
ytmnd_id = match.group(1)
response = requests.get("http://" + domain + ".ytmnd.com/info/" + ytmnd_id + "/json",
headers={'User-Agent': 'Mozilla/5.0'})
response = requests.get(
"http://" + domain + ".ytmnd.com/info/" + ytmnd_id + "/json",
headers={"User-Agent": "Mozilla/5.0"},
)
response.raise_for_status()
ytmnd_info = response.json()
@@ -121,93 +124,260 @@ class YTMND:
return ytmnd_info
def fetch_media(self, ytmnd_info):
domain = ytmnd_info['site']['domain']
original_gif = ytmnd_info['site']['foreground']['url']
domain = ytmnd_info["site"]["domain"]
original_gif = ytmnd_info["site"]["foreground"]["url"]
gif_type = original_gif.split(".")[-1]
original_wav = ytmnd_info['site']['sound']['url']
wav_type = ytmnd_info['site']['sound']['type']
original_wav = ytmnd_info["site"]["sound"]["url"]
wav_type = ytmnd_info["site"]["sound"]["type"]
if 'alternates' in ytmnd_info['site']['sound']:
key = list(ytmnd_info['site']['sound']['alternates'].keys())[0]
value = ytmnd_info['site']['sound']['alternates'][key]
if value['file_type'] != 'swf':
original_wav = value['file_url']
wav_type = ytmnd_info['site']['sound']['file_type']
if "alternates" in ytmnd_info["site"]["sound"]:
key = list(ytmnd_info["site"]["sound"]["alternates"].keys())[0]
value = ytmnd_info["site"]["sound"]["alternates"][key]
if value["file_type"] != "swf":
original_wav = value["file_url"]
wav_type = ytmnd_info["site"]["sound"]["file_type"]
subprocess.run(["wget", "--quiet", "-O", f"{domain}.{gif_type}", original_gif])
subprocess.run(["wget", "--quiet", "-O", f"{domain}.{wav_type}", original_wav])
def write_index(self, ytmnd_info):
domain = ytmnd_info["site"]["domain"]
bgcolor = ytmnd_info["site"]["background"]["color"]
title = ytmnd_info["site"]["description"]
placement = ytmnd_info["site"]["foreground"]["placement"]
domain = ytmnd_info['site']['domain']
bgcolor = ytmnd_info['site']['background']['color']
title = ytmnd_info['site']['description']
placement = ytmnd_info['site']['foreground']['placement']
original_gif = ytmnd_info['site']['foreground']['url']
original_gif = ytmnd_info["site"]["foreground"]["url"]
gif_type = original_gif.split(".")[-1]
wav_type = ytmnd_info['site']['sound']['type']
wav_type = ytmnd_info["site"]["sound"]["type"]
if 'alternates' in ytmnd_info['site']['sound']:
key = list(ytmnd_info['site']['sound']['alternates'].keys())[0]
value = ytmnd_info['site']['sound']['alternates'][key]
if value['file_type'] != 'swf':
original_wav = value['file_url']
wav_type = ytmnd_info['site']['sound']['file_type']
if "alternates" in ytmnd_info["site"]["sound"]:
key = list(ytmnd_info["site"]["sound"]["alternates"].keys())[0]
value = ytmnd_info["site"]["sound"]["alternates"][key]
if value["file_type"] != "swf":
original_wav = value["file_url"]
wav_type = ytmnd_info["site"]["sound"]["file_type"]
with open(domain + ".html", 'w', encoding='utf-8') as fn:
with open(domain + ".html", "w", encoding="utf-8") as fn:
fn.write("<!DOCTYPE html>\n")
fn.write("<html>\n")
fn.write("<head>\n")
fn.write("<meta charset='utf-8'>\n")
fn.write(
"<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n"
)
fn.write("<title>%s</title>\n" % title)
fn.write("<style>\n")
fn.write("*{margin:0;padding:0;width:100%;height:100%;}\n")
fn.write("body{font-size:12px;font-weight:normal;font-style:normal;overflow:hidden;")
fn.write(
"body{font-size:12px;font-weight:normal;font-style:normal;overflow:hidden;"
)
fn.write("background-color:%s;" % bgcolor)
fn.write("background-image:url(%s.%s);" % (domain, gif_type))
if placement == "mc":
fn.write("background-position: center center; background-repeat: no-repeat;}")
fn.write(
"background-position: center center; background-repeat: no-repeat;}"
)
elif placement == "tile":
fn.write("background-position: top left; background-repeat: repeat;}")
fn.write("\n")
fn.write("#zoom_text{position:absolute;left:0;top:0;width:1000px;z-index:10;text-align:center;font-family:Tahoma, sans-serif}")
fn.write("#zoom_text div{position:absolute;width:1000px}")
fn.write(
"#zoom_text{position:absolute;left:0;top:0;width:1000px;z-index:10;text-align:center;font-family:Tahoma, sans-serif}\n"
)
fn.write("#zoom_text div{position:absolute;width:1000px}\n")
fn.write(
"#unmute-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;z-index:99999;cursor:pointer;}\n"
)
fn.write(
"#unmute-btn{width:80px;height:80px;background:rgba(255,255,255,0.1);border:2px solid rgba(255,255,255,0.3);border-radius:50%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;font-size:32px;color:rgba(255,255,255,0.9);-webkit-transition:all 0.3s ease;transition:all 0.3s ease;}\n"
)
fn.write(
"#unmute-btn:hover{background:rgba(255,255,255,0.2);border-color:rgba(255,255,255,0.5);-webkit-transform:scale(1.1);-ms-transform:scale(1.1);transform:scale(1.1);}\n"
)
fn.write("</style>\n")
fn.write("</head>\n")
fn.write("<body>\n")
fn.write('<div id="unmute-overlay">\n')
fn.write(' <div id="unmute-btn">▶</div>\n')
fn.write("</div>\n")
self.write_zoom_text(fn, ytmnd_info)
if self.no_web_audio:
fn.write("<audio src='%s.%s' loop autoplay>\n" % (domain, wav_type))
fn.write("</body>\n")
else:
fn.write("</body>\n")
fn.write("<script>var url = '%s.%s'</script>\n" % (domain, wav_type))
fn.write("<script src='ytmnd.js'></script>\n")
fn.write("<script type='application/json'>\n")
fn.write(json.dumps(ytmnd_info, sort_keys=True, indent=4) + "\n")
fn.write("<script>\n")
fn.write("(function() {\n")
fn.write(" var audioUrl = '%s.%s';\n" % (domain, wav_type))
fn.write(" var context = null;\n")
fn.write(" var source = null;\n")
fn.write(" var audioBuffer = null;\n")
fn.write(" var isPlaying = false;\n")
fn.write(" var fallbackAudio = null;\n")
fn.write(" \n")
fn.write(" function hasWebAudio() {\n")
fn.write(
" return ('AudioContext' in window) || ('webkitAudioContext' in window);\n"
)
fn.write(" }\n")
fn.write(" \n")
fn.write(" function createContext() {\n")
fn.write(" if ('AudioContext' in window) {\n")
fn.write(" return new AudioContext();\n")
fn.write(" } else if ('webkitAudioContext' in window) {\n")
fn.write(" return new webkitAudioContext();\n")
fn.write(" }\n")
fn.write(" return null;\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" function loadAudioWithXHR(callback, errorCallback) {\n")
fn.write(" var request = new XMLHttpRequest();\n")
fn.write(" request.open('GET', audioUrl, true);\n")
fn.write(" request.responseType = 'arraybuffer';\n")
fn.write(" request.onload = function() {\n")
fn.write(" if (request.status === 200) {\n")
fn.write(" callback(request.response);\n")
fn.write(" } else {\n")
fn.write(
" errorCallback('Request failed with status: ' + request.status);\n"
)
fn.write(" }\n")
fn.write(" };\n")
fn.write(" request.onerror = function() {\n")
fn.write(" errorCallback('Network error');\n")
fn.write(" };\n")
fn.write(" request.send();\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" function loopAudio() {\n")
fn.write(" if (!isPlaying || !audioBuffer) return;\n")
fn.write(" \n")
fn.write(" source = context.createBufferSource();\n")
fn.write(" source.connect(context.destination);\n")
fn.write(" source.buffer = audioBuffer;\n")
fn.write(" \n")
fn.write(" try {\n")
fn.write(" if (source.start) {\n")
fn.write(" source.start(0);\n")
fn.write(" } else if (source.noteOn) {\n")
fn.write(" source.noteOn(0);\n")
fn.write(" }\n")
fn.write(" } catch(e) {\n")
fn.write(" console.error('Start error:', e);\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" var duration = audioBuffer.duration * 1000;\n")
fn.write(" var offset = audioBuffer.duration < 2 ? 0 : 60;\n")
fn.write(" setTimeout(loopAudio, duration - offset);\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" function playWebAudio() {\n")
fn.write(" context = createContext();\n")
fn.write(" if (!context) {\n")
fn.write(" fallbackToHTMLAudio();\n")
fn.write(" return;\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" if (context.state === 'suspended') {\n")
fn.write(" context.resume();\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" loadAudioWithXHR(\n")
fn.write(" function(arrayBuffer) {\n")
fn.write(" context.decodeAudioData(\n")
fn.write(" arrayBuffer,\n")
fn.write(" function(buffer) {\n")
fn.write(" audioBuffer = buffer;\n")
fn.write(" isPlaying = true;\n")
fn.write(" loopAudio();\n")
fn.write(" },\n")
fn.write(" function(error) {\n")
fn.write(" console.error('Decode error:', error);\n")
fn.write(" fallbackToHTMLAudio();\n")
fn.write(" }\n")
fn.write(" );\n")
fn.write(" },\n")
fn.write(" function(error) {\n")
fn.write(" console.error('Load error:', error);\n")
fn.write(" fallbackToHTMLAudio();\n")
fn.write(" }\n")
fn.write(" );\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" function fallbackToHTMLAudio() {\n")
fn.write(" try {\n")
fn.write(" fallbackAudio = new Audio(audioUrl);\n")
fn.write(" fallbackAudio.loop = true;\n")
fn.write(" var playPromise = fallbackAudio.play();\n")
fn.write(" if (playPromise && playPromise.catch) {\n")
fn.write(" playPromise.catch(function(error) {\n")
fn.write(" console.error('HTML5 audio play failed:', error);\n")
fn.write(" });\n")
fn.write(" }\n")
fn.write(" isPlaying = true;\n")
fn.write(" } catch(e) {\n")
fn.write(" console.error('Fallback audio failed:', e);\n")
fn.write(" }\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" function startAudio() {\n")
fn.write(" var overlay = document.getElementById('unmute-overlay');\n")
fn.write(" if (overlay) {\n")
fn.write(" overlay.style.display = 'none';\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" if (hasWebAudio()) {\n")
fn.write(" playWebAudio();\n")
fn.write(" } else {\n")
fn.write(" fallbackToHTMLAudio();\n")
fn.write(" }\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" var overlay = document.getElementById('unmute-overlay');\n")
fn.write(" if (overlay) {\n")
fn.write(" overlay.addEventListener('click', startAudio);\n")
fn.write(" overlay.addEventListener('touchstart', function(e) {\n")
fn.write(" e.preventDefault();\n")
fn.write(" startAudio();\n")
fn.write(" });\n")
fn.write(" }\n")
fn.write(" \n")
fn.write(" document.addEventListener('keydown', function(e) {\n")
fn.write(" if (overlay && overlay.style.display !== 'none') {\n")
fn.write(" startAudio();\n")
fn.write(" }\n")
fn.write(" });\n")
fn.write("})();\n")
fn.write("</script>\n")
fn.write("<script type='application/json' id='ytmnd-data'>\n")
fn.write(json.dumps(ytmnd_info, sort_keys=True, indent=2) + "\n")
fn.write("</script>\n")
fn.write("</html>")
def write_zoom_text(self, fn, ytmnd_info):
if 'zoom_text' not in ytmnd_info['site']:
if "zoom_text" not in ytmnd_info["site"]:
return
zoom_text = ytmnd_info['site']['zoom_text']
zoom_text = ytmnd_info["site"]["zoom_text"]
fn.write('<div id="zoom_text">')
offset = 100
if "line_3" in zoom_text and len(zoom_text["line_3"]) > 0:
self.write_zoom_layers(fn, zoom_text['line_3'], offset, 269)
self.write_zoom_layers(fn, zoom_text["line_3"], offset, 269)
offset += 21
if "line_2" in zoom_text and len(zoom_text["line_2"]) > 0:
self.write_zoom_layers(fn, zoom_text['line_2'], offset, 135)
self.write_zoom_layers(fn, zoom_text["line_2"], offset, 135)
offset += 21
if "line_1" in zoom_text and len(zoom_text["line_1"]) > 0:
self.write_zoom_layers(fn, zoom_text['line_1'], offset, 1)
self.write_zoom_layers(fn, zoom_text["line_1"], offset, 1)
fn.write('</div>')
fn.write("</div>")
def write_zoom_layers(self, fn, text, offset, top):
for i in range(1, 22):
@@ -220,63 +390,59 @@ class YTMND:
else:
color = i * 4
fn.write("<div style='z-index: %d; left: %dpx; top: %dpx; color: rgb(%d, %d, %d); font-size: %dpt;'>%s</div>"
% (z_index, row_left, row_top, color, color, color, font_size, text))
def copy_ytmnd_js(self):
if not os.path.isfile("ytmnd.js"):
parent_js = os.path.join("..", "ytmnd.js")
if os.path.isfile(parent_js):
subprocess.run(["cp", parent_js, "."])
fn.write(
"<div style='z-index: %d; left: %dpx; top: %dpx; color: rgb(%d, %d, %d); font-size: %dpt;'>%s</div>"
% (z_index, row_left, row_top, color, color, color, font_size, text)
)
def parse_json(self, ytmnd_info):
domain = ytmnd_info['site']['domain']
bgcolor = ytmnd_info['site']['background']['color']
title = ytmnd_info['site']['description']
placement = ytmnd_info['site']['foreground']['placement']
domain = ytmnd_info["site"]["domain"]
bgcolor = ytmnd_info["site"]["background"]["color"]
title = ytmnd_info["site"]["description"]
placement = ytmnd_info["site"]["foreground"]["placement"]
gif_type = ytmnd_info['site']['foreground']['url'].split(".")[-1]
wav_type = ytmnd_info['site']['sound']['type']
zoom_text = ytmnd_info['site']['zoom_text']
keywords = ytmnd_info['site']['keywords']
username = ytmnd_info['site']['user']['user_name']
sound_origin = ytmnd_info['site']['sound_origin']
image_origin = ytmnd_info['site']['fg_image_origin']
work_safe = ytmnd_info['site']['work_safe']
gif_type = ytmnd_info["site"]["foreground"]["url"].split(".")[-1]
wav_type = ytmnd_info["site"]["sound"]["type"]
zoom_text = ytmnd_info["site"]["zoom_text"]
keywords = ytmnd_info["site"]["keywords"]
username = ytmnd_info["site"]["user"]["user_name"]
sound_origin = ytmnd_info["site"]["sound_origin"]
image_origin = ytmnd_info["site"]["fg_image_origin"]
work_safe = ytmnd_info["site"]["work_safe"]
if len(zoom_text['line_1']) == 0:
if len(zoom_text["line_1"]) == 0:
zoom_text = ""
if 'alternates' in ytmnd_info['site']['sound']:
key = list(ytmnd_info['site']['sound']['alternates'].keys())[0]
value = ytmnd_info['site']['sound']['alternates'][key]
if value['file_type'] != 'swf':
wav_type = ytmnd_info['site']['sound']['file_type']
if "alternates" in ytmnd_info["site"]["sound"]:
key = list(ytmnd_info["site"]["sound"]["alternates"].keys())[0]
value = ytmnd_info["site"]["sound"]["alternates"][key]
if value["file_type"] != "swf":
wav_type = ytmnd_info["site"]["sound"]["file_type"]
simplified_info = {
'domain': domain,
'title': title,
'username': username,
'work_safe': work_safe,
'bgcolor': bgcolor,
'placement': placement,
'zoom_text': zoom_text,
'image': domain + "." + gif_type,
'sound': domain + "." + wav_type,
'image_type': gif_type,
'sound_type': wav_type,
'image_origin': image_origin,
'sound_origin': sound_origin,
"domain": domain,
"title": title,
"username": username,
"work_safe": work_safe,
"bgcolor": bgcolor,
"placement": placement,
"zoom_text": zoom_text,
"image": domain + "." + gif_type,
"sound": domain + "." + wav_type,
"image_type": gif_type,
"sound_type": wav_type,
"image_origin": image_origin,
"sound_origin": sound_origin,
}
return simplified_info
def write_json(self, domain, data):
with open(domain + '.json', 'w', encoding='utf-8') as fn:
with open(domain + ".json", "w", encoding="utf-8") as fn:
fn.write(json.dumps(data))
if __name__ == '__main__':
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-u", "--user", action="store_true")
@@ -285,7 +451,9 @@ if __name__ == '__main__':
parser.add_option("-j", "--json-only", action="store_true")
parser.add_option("-w", "--no-web-audio", action="store_true")
parser.add_option("-p", "--print-json", action="store_true")
parser.add_option("-s", "--sleep", action="store", type="int", dest="sleep", default=5)
parser.add_option(
"-s", "--sleep", action="store", type="int", dest="sleep", default=5
)
(options, args) = parser.parse_args()
@@ -307,5 +475,11 @@ if __name__ == '__main__':
ytmnd.fetch_user(user)
else:
name = args[0].replace("http://","").replace(".ytmnsfw.com","").replace(".ytmnd.com","").replace("/","")
name = (
args[0]
.replace("http://", "")
.replace(".ytmnsfw.com", "")
.replace(".ytmnd.com", "")
.replace("/", "")
)
ytmnd.fetch_ytmnd(name)