Merge branch 'main' of github.com:monochrome-music/monochrome

This commit is contained in:
Samidy 2026-04-05 16:58:54 +03:00
commit 9ccc7c748b
14 changed files with 305 additions and 104 deletions

View file

@ -1,39 +1,38 @@
name: "Copilot Setup Steps" name: 'Copilot Setup Steps'
# Automatically run the setup steps when they are changed to allow for easy validation, and # Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab # allow manual testing through the repository's "Actions" tab
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
paths: paths:
- .github/workflows/copilot-setup-steps.yml - .github/workflows/copilot-setup-steps.yml
pull_request: pull_request:
paths: paths:
- .github/workflows/copilot-setup-steps.yml - .github/workflows/copilot-setup-steps.yml
jobs: jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps: copilot-setup-steps:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Set the permissions to the lowest permissions possible needed for your steps. # Set the permissions to the lowest permissions possible needed for your steps.
# Copilot will be given its own token for its operations. # Copilot will be given its own token for its operations.
permissions: permissions:
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. contents: read
# If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. workflows: write
contents: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: latest bun-version: latest
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Install playwright dependencies - name: Install playwright dependencies
run: bun run install:playwright run: bun run install:playwright

View file

@ -25,7 +25,7 @@ jobs:
echo "Files changed in this commit:" echo "Files changed in this commit:"
echo "$CHANGED" echo "$CHANGED"
if echo "$CHANGED" | grep -qE '^public/editors-picks\.json$|^public/editors-picks-old/'; then if echo "$CHANGED" | grep -qE '^public/editors-picks\.json$|^public/editors-picks-old/'; then
echo "Detected changes to generated files in this commit backing off to avoid overwriting manual edits." echo "Detected changes to generated files in this commit - backing off to avoid overwriting manual edits."
echo "skip=true" >> "$GITHUB_OUTPUT" echo "skip=true" >> "$GITHUB_OUTPUT"
else else
echo "skip=false" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT"
@ -37,6 +37,10 @@ jobs:
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install webp
if: steps.backoff.outputs.skip == 'false'
run: sudo apt-get update && sudo apt-get install -y webp
- name: Archive current editors picks - name: Archive current editors picks
if: steps.backoff.outputs.skip == 'false' if: steps.backoff.outputs.skip == 'false'
run: | run: |
@ -60,9 +64,19 @@ jobs:
label = m.group(1).strip() label = m.group(1).strip()
break break
# Copy current picks to archive # Copy current picks to archive, replacing monochrome URLs with UUIDs
with open("public/editors-picks.json") as f: with open("public/editors-picks.json") as f:
current = json.load(f) current = json.load(f)
# Replace cover URLs with original UUIDs
url_pattern = re.compile(r'^https://monochrome\.tf/editors-picks-images/([a-f0-9-]+)\.webp$')
for item in current:
for field in ['cover', 'picture', 'image']:
if field in item and item[field]:
match = url_pattern.match(item[field])
if match:
item[field] = match.group(1)
with open(archive_path, "w") as f: with open(archive_path, "w") as f:
json.dump(current, f, indent=4) json.dump(current, f, indent=4)
@ -89,7 +103,7 @@ jobs:
run: | run: |
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
git add public/editors-picks.json public/editors-picks-old/ git add public/editors-picks.json public/editors-picks-old/ public/editors-picks-images/
git diff --staged --quiet && echo "No changes to commit." && exit 0 git diff --staged --quiet && echo "No changes to commit." && exit 0
git commit -m "chore: update editors picks" git commit -m "chore: update editors picks"
git push git push

View file

@ -21,3 +21,4 @@ album:250986538
album:509761344 album:509761344
album:15621057 album:15621057
album:103897783 album:103897783
album:151728406

View file

@ -7,12 +7,17 @@ import re
import sys import sys
import hashlib import hashlib
import time import time
import os
import subprocess
import shutil
import tempfile
INPUT_FILE = "editors-picks-input.txt" INPUT_FILE = "editors-picks-input.txt"
IMAGES_DIR = "public/editors-picks-images"
COUNTRY = "US" COUNTRY = "US"
# Tidal internal token replace when expired # Tidal internal token replace when expired
TIDAL_TOKEN = "eyJraWQiOiJ2OU1GbFhqWSIsImFsZyI6IkVTMjU2In0.eyJ0eXBlIjoibzJfYWNjZXNzIiwic2NvcGUiOiIiLCJnVmVyIjowLCJzVmVyIjowLCJjaWQiOjEzNTU3LCJhdCI6IklOVEVSTkFMIiwiZXhwIjoxNzc1MjQ2NzQ2LCJpc3MiOiJodHRwczovL2F1dGgudGlkYWwuY29tL3YxIn0.ksUE4yhQ39IG7oHWk8DyJ91dwIoDVWGzvTAnpeDJ5p-_Gp0F_yO858xDO11AINBaahQCq0jlbqWqIaTqCTOjqg" TIDAL_TOKEN = "eyJraWQiOiJ2OU1GbFhqWSIsImFsZyI6IkVTMjU2In0.eyJ0eXBlIjoibzJfYWNjZXNzIiwic2NvcGUiOiIiLCJnVmVyIjowLCJzVmVyIjowLCJjaWQiOjEzNTU3LCJhdCI6IklOVEVSTkFMIiwiZXhwIjoxNzc1MzY0MTQwLCJpc3MiOiJodHRwczovL2F1dGgudGlkYWwuY29tL3YxIn0.6ui6itHVQ-OXPF0F9mbf5KcKz1fKYJNsa1vBAj60upXpcN-DQG8JPKBlqJN6RuBEH8yhwYj2wh4YJ-TOOuO8DA"
TIDAL_HEADERS = { TIDAL_HEADERS = {
"accept": "*/*", "accept": "*/*",
@ -83,6 +88,61 @@ def fetch_podcast(feed_id):
return podcast_get(f"/podcasts/byfeedid?id={feed_id}&pretty") return podcast_get(f"/podcasts/byfeedid?id={feed_id}&pretty")
# ── Image processing ───────────────────────────────────────────────────────────
def clear_images_dir():
if os.path.exists(IMAGES_DIR):
shutil.rmtree(IMAGES_DIR)
os.makedirs(IMAGES_DIR, exist_ok=True)
def is_uuid_cover(cover_value):
if not cover_value:
return False
return bool(re.match(r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$', cover_value))
def uuid_to_path_segments(uuid):
return uuid.replace('-', '/')
def download_and_process_cover(cover_uuid):
url = f"https://resources.tidal.com/images/{uuid_to_path_segments(cover_uuid)}/320x320.jpg"
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
tmp_path = tmp.name
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req) as resp:
with open(tmp_path, 'wb') as f:
shutil.copyfileobj(resp, f)
output_path = os.path.join(IMAGES_DIR, f"{cover_uuid}.webp")
subprocess.run(
['cwebp', '-q', '50', tmp_path, '-o', output_path],
check=True,
capture_output=True
)
return f"https://monochrome.tf/editors-picks-images/{cover_uuid}.webp"
except Exception as e:
print(f"Error processing cover {cover_uuid}: {e}", file=sys.stderr)
return None
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def process_cover(cover_value):
if not cover_value:
return cover_value
if is_uuid_cover(cover_value):
return download_and_process_cover(cover_value)
return cover_value
# ── Transformers ────────────────────────────────────────────────────────────── # ── Transformers ──────────────────────────────────────────────────────────────
def transform_album(d): def transform_album(d):
@ -95,7 +155,7 @@ def transform_album(d):
"name": d.get("artist", {}).get("name"), "name": d.get("artist", {}).get("name"),
}, },
"releaseDate": d.get("releaseDate"), "releaseDate": d.get("releaseDate"),
"cover": d.get("cover"), "cover": process_cover(d.get("cover")),
"explicit": d.get("explicit"), "explicit": d.get("explicit"),
"audioQuality": d.get("audioQuality"), "audioQuality": d.get("audioQuality"),
"mediaMetadata": d.get("mediaMetadata"), "mediaMetadata": d.get("mediaMetadata"),
@ -107,7 +167,7 @@ def transform_artist(d):
"type": "artist", "type": "artist",
"id": d.get("id"), "id": d.get("id"),
"name": d.get("name"), "name": d.get("name"),
"picture": d.get("picture"), "picture": process_cover(d.get("picture")),
} }
@ -124,7 +184,7 @@ def transform_track(d):
"album": { "album": {
"id": album.get("id"), "id": album.get("id"),
"title": album.get("title"), "title": album.get("title"),
"cover": album.get("cover"), "cover": process_cover(album.get("cover")),
}, },
"duration": d.get("duration"), "duration": d.get("duration"),
"explicit": d.get("explicit"), "explicit": d.get("explicit"),
@ -140,7 +200,7 @@ def transform_playlist(d):
"type": "playlist", "type": "playlist",
"id": d.get("uuid"), "id": d.get("uuid"),
"title": d.get("title"), "title": d.get("title"),
"cover": cover, "cover": process_cover(cover),
"numberOfTracks": d.get("numberOfTracks", 0), "numberOfTracks": d.get("numberOfTracks", 0),
} }
@ -153,7 +213,7 @@ def transform_userplaylist(d):
"type": "user-playlist", "type": "user-playlist",
"id": d.get("uuid"), "id": d.get("uuid"),
"name": d.get("title"), "name": d.get("title"),
"cover": cover, "cover": process_cover(cover),
"numberOfTracks": d.get("numberOfTracks", 0), "numberOfTracks": d.get("numberOfTracks", 0),
"username": creator.get("name"), "username": creator.get("name"),
} }
@ -198,6 +258,8 @@ def read_items(path):
# ── Main ────────────────────────────────────────────────────────────────────── # ── Main ──────────────────────────────────────────────────────────────────────
clear_images_dir()
FETCHERS = { FETCHERS = {
"album": (fetch_album, transform_album), "album": (fetch_album, transform_album),
"artist": (fetch_artist, transform_artist), "artist": (fetch_artist, transform_artist),
@ -212,7 +274,7 @@ picks = []
for item_type, item_id in items: for item_type, item_id in items:
if item_type not in FETCHERS: if item_type not in FETCHERS:
print(f"Unknown type '{item_type}' for id {item_id!r} skipping", file=sys.stderr) print(f"Unknown type '{item_type}' for id {item_id!r} - skipping", file=sys.stderr)
continue continue
fetch_fn, transform_fn = FETCHERS[item_type] fetch_fn, transform_fn = FETCHERS[item_type]
data = fetch_fn(item_id) data = fetch_fn(item_id)

View file

@ -3,24 +3,79 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Monochrome Music</title> <title>Monochrome</title>
<link rel="canonical" href="https://monochrome.tf/" /> <link rel="canonical" href="https://monochrome.tf/" />
<link rel="alternate" hreflang="en" href="https://monochrome.tf/" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<base href="/" /> <base href="/" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Monochrome" /> <meta name="apple-mobile-web-app-title" content="Monochrome" />
<meta name="description" content="A minimalist music streaming application" /> <meta
name="description"
content="Stream and download millions of Hi-Res FLACs, unreleased songs and music videos, all for free on Monochrome."
/>
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="googlebot" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="Monochrome" />
<meta property="og:title" content="Monochrome" />
<meta
property="og:description"
content="Stream and download millions of Hi-Res FLACs, unreleased songs and music videos, all for free on Monochrome."
/>
<meta property="og:url" content="https://monochrome.tf/" />
<meta property="og:image" content="https://monochrome.tf/assets/banner.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content="Monochrome banner" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://monochrome.tf/" />
<meta name="twitter:title" content="Monochrome" />
<meta
name="twitter:description"
content="Stream and download millions of Hi-Res FLACs, unreleased songs and music videos, all for free on Monochrome."
/>
<meta name="twitter:image" content="https://monochrome.tf/assets/banner-twitter.jpg" />
<meta name="twitter:image:alt" content="Monochrome banner" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Monochrome",
"url": "https://monochrome.tf/",
"description": "Stream and download millions of Hi-Res FLACs, unreleased songs and music videos, all for free on Monochrome.",
"potentialAction": {
"@type": "SearchAction",
"target": "https://monochrome.tf/search/{search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Monochrome",
"url": "https://monochrome.tf/",
"applicationCategory": "MusicApplication",
"operatingSystem": "Web Browser",
"image": "https://monochrome.tf/assets/logo.svg",
"description": "Stream and download millions of Hi-Res FLACs, unreleased songs and music videos, all for free on Monochrome."
}
</script>
<!-- bini's seo thing -->
<meta
name="ahrefs-site-verification"
content="889c6a5c1584832256b87bb36beaa665b40bb6e201582d416f04d074794044ef"
/>
<meta name="google-site-verification" content="ChAPlKFJ5Dk2YbWhiUfnH5sHgCC8SnNdCBtjVcahArY" />
<!-- Preconnect to critical third-party origins --> <!-- Preconnect to critical third-party origins -->
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin /> <link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin />
<link rel="preconnect" href="https://ws.audioscrobbler.com" crossorigin />
<link rel="preconnect" href="https://libre.fm" crossorigin />
<link rel="preconnect" href="https://api.listenbrainz.org" crossorigin />
<link rel="preconnect" href="https://resources.tidal.com" crossorigin /> <link rel="preconnect" href="https://resources.tidal.com" crossorigin />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin /> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://unpkg.com" crossorigin />
<link rel="apple-touch-icon" href="/assets/logo.svg" /> <link rel="apple-touch-icon" href="/assets/logo.svg" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />

View file

@ -127,32 +127,36 @@ async function loadDownloadsModule() {
} }
async function fetchcontributors() { async function fetchcontributors() {
const response = await fetch('https://api.samidy.com/api/contributors'); try {
const data1 = await response.json(); const response = await fetch('https://api.samidy.com/api/contributors');
if (!response.ok) return;
const data1 = await response.json();
const data = data1.filter( const data = data1.filter(
(user) => user.type !== 'Bot' && user.login !== 'edidealt' && user.login !== 'satanyahoo' (user) => user.type !== 'Bot' && user.login !== 'edidealt' && user.login !== 'satanyahoo'
); );
const edideaur = data.find((user) => user.login === 'edideaur'); const edideaur = data.find((user) => user.login === 'edideaur');
if (edideaur) { if (edideaur) {
edideaur.contributions += data1.find((u) => u.login === 'edidealt')?.contributions || 0; edideaur.contributions += data1.find((u) => u.login === 'edidealt')?.contributions || 0;
edideaur.contributions += data1.find((u) => u.login === 'satanyahoo')?.contributions || 0; edideaur.contributions += data1.find((u) => u.login === 'satanyahoo')?.contributions || 0;
} }
const con = document.querySelector('.about-contributors'); const con = document.querySelector('.about-contributors');
if (!con) return;
data.forEach((user) => { data.forEach((user) => {
const userDIV = document.createElement('div'); const userDIV = document.createElement('div');
userDIV.innerHTML = ` userDIV.innerHTML = `
<a href="${user.html_url}" target="_blank"> <a href="${user.html_url}" target="_blank">
<img src="${user.avatar_url}" alt="${user.login}" width="50" style="border-radius: 50%;"> <img src="${user.avatar_url}" alt="${user.login}" width="50" style="border-radius: 50%;">
<span>${user.login}</span> <span>${user.login}</span>
<span class="contrib">Contributions: ${user.contributions}</span> <span class="contrib">Contributions: ${user.contributions}</span>
</a> </a>
`; `;
con.appendChild(userDIV); con.appendChild(userDIV);
}); });
} catch (e) {}
} }
async function loadMetadataModule() { async function loadMetadataModule() {
@ -500,7 +504,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize tracker // Initialize tracker
initTracker().catch(console.error); initTracker().catch(console.error);
fetchcontributors(); await fetchcontributors();
const castBtn = document.getElementById('cast-btn'); const castBtn = document.getElementById('cast-btn');
initializeCasting(audioPlayer, castBtn); initializeCasting(audioPlayer, castBtn);

View file

@ -278,13 +278,13 @@ export class LyricsManager {
// Load Kuroshiro from CDN // Load Kuroshiro from CDN
if (!window.Kuroshiro) { if (!window.Kuroshiro) {
await this.loadScript('https://unpkg.com/kuroshiro@1.2.0/dist/kuroshiro.min.js'); await this.loadScript('https://cdn.jsdelivr.net/npm/kuroshiro@1.2.0/dist/kuroshiro.min.js');
} }
// Load Kuromoji analyzer from CDN // Load Kuromoji analyzer from CDN
if (!window.KuromojiAnalyzer) { if (!window.KuromojiAnalyzer) {
await this.loadScript( await this.loadScript(
'https://unpkg.com/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js' 'https://cdn.jsdelivr.net/npm/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js'
); );
} }

View file

@ -487,7 +487,7 @@ export class Player {
const timeRemaining = duration - currentTime; const timeRemaining = duration - currentTime;
// Preload if we are in last 30 seconds of song // Preload if we are in last 30 seconds of song
const shouldPreload = (duration > 0 && timeRemaining <= 30); const shouldPreload = duration > 0 && timeRemaining <= 30;
if (shouldPreload) { if (shouldPreload) {
this._pendingPreload = false; this._pendingPreload = false;
@ -537,7 +537,7 @@ export class Player {
albumPeakAmplitude: trackData.info.albumPeakAmplitude, albumPeakAmplitude: trackData.info.albumPeakAmplitude,
}; };
} }
} catch(e) {} // Fail silently } catch (_e) {} // Fail silently
} }
this.preloadCache.set(track.id, streamInfo); this.preloadCache.set(track.id, streamInfo);
@ -546,12 +546,19 @@ export class Player {
// Warm connection and pre-fetch // Warm connection and pre-fetch
if (!streamUrl.startsWith('blob:')) { if (!streamUrl.startsWith('blob:')) {
if (streamUrl.includes('.mpd') || streamUrl.includes('.m3u8')) { if (streamUrl.includes('.mpd') || streamUrl.includes('.m3u8')) {
if (this.shakaInitialized && this.shakaPlayer && typeof this.shakaPlayer.preload === 'function') { if (
this.shakaInitialized &&
this.shakaPlayer &&
typeof this.shakaPlayer.preload === 'function'
) {
try { try {
let preloadConfig = undefined; let preloadConfig = undefined;
if (typeof this.shakaPlayer.getConfiguration === 'function') { if (typeof this.shakaPlayer.getConfiguration === 'function') {
preloadConfig = this.shakaPlayer.getConfiguration(); preloadConfig = this.shakaPlayer.getConfiguration();
const stats = typeof this.shakaPlayer.getStats === 'function' ? this.shakaPlayer.getStats() : null; const stats =
typeof this.shakaPlayer.getStats === 'function'
? this.shakaPlayer.getStats()
: null;
if (stats && stats.estimatedBandwidth) { if (stats && stats.estimatedBandwidth) {
preloadConfig.abr.defaultBandwidthEstimate = stats.estimatedBandwidth; preloadConfig.abr.defaultBandwidthEstimate = stats.estimatedBandwidth;
} }
@ -560,16 +567,24 @@ export class Player {
// which forces the player to discard and re-fetch chunks on slow connections. // which forces the player to discard and re-fetch chunks on slow connections.
preloadConfig.abr.enabled = false; preloadConfig.abr.enabled = false;
try { try {
const variants = typeof this.shakaPlayer.getVariantTracks === 'function' ? this.shakaPlayer.getVariantTracks() : []; const variants =
const activeVariant = variants.find(v => v.active); typeof this.shakaPlayer.getVariantTracks === 'function'
? this.shakaPlayer.getVariantTracks()
: [];
const activeVariant = variants.find((v) => v.active);
if (activeVariant && activeVariant.audioCodec) { if (activeVariant && activeVariant.audioCodec) {
preloadConfig.preferredAudioCodecs = [activeVariant.audioCodec]; preloadConfig.preferredAudioCodecs = [activeVariant.audioCodec];
} }
} catch (e) {} } catch (_e) {}
} }
const preloadManager = await this.shakaPlayer.preload(streamUrl, null, null, preloadConfig); const preloadManager = await this.shakaPlayer.preload(
streamUrl,
null,
null,
preloadConfig
);
streamInfo.preloadManager = preloadManager; streamInfo.preloadManager = preloadManager;
} catch (e) { } catch (_e) {
// Ignore preload errors, will just load fresh // Ignore preload errors, will just load fresh
} }
} else { } else {
@ -1015,14 +1030,17 @@ export class Player {
} else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) { } else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) {
await this.shakaPlayer.attach(activeElement); await this.shakaPlayer.attach(activeElement);
const loadTarget = track.type == 'video' && this.preloadCache.has(track.id) ? const loadTarget =
(this.preloadCache.get(track.id).preloadManager || streamUrl) : streamUrl; track.type == 'video' && this.preloadCache.has(track.id)
? this.preloadCache.get(track.id).preloadManager || streamUrl
: streamUrl;
try { try {
await this.shakaPlayer.load(loadTarget); await this.shakaPlayer.load(loadTarget);
} catch (e) { } catch (e) {
console.error("PreloadManager load Error:", e); if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl); console.error('PreloadManager load Error:', e);
else throw e; if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
else throw e;
} }
this.shakaInitialized = true; this.shakaInitialized = true;
@ -1097,8 +1115,9 @@ export class Player {
await this.shakaPlayer.load(loadTarget); await this.shakaPlayer.load(loadTarget);
} }
} catch (e) { } catch (e) {
console.error("PreloadManager load Error:", e); if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl); console.error('PreloadManager load Error:', e);
else throw e; if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
else throw e;
} }
this.shakaInitialized = true; this.shakaInitialized = true;

View file

@ -145,7 +145,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
if (explicit !== undefined) { if (explicit !== undefined) {
if (isMp4) { if (isMp4) {
// rtng is a byte item must be set directly on the Mp4Tag // rtng is a byte item - must be set directly on the Mp4Tag
const mp4Tag = underlying.tag(); const mp4Tag = underlying.tag();
mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0)); mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0));
} else { } else {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

11
public/robots.txt Normal file
View file

@ -0,0 +1,11 @@
# robots.txt for https://monochrome.tf/
User-agent: *
Allow: /
# Avoid indexing internal endpoints and auth flows.
Disallow: /functions/
Disallow: /api/
Disallow: /auth/
Sitemap: https://monochrome.tf/sitemap.xml

43
public/sitemap.xml Normal file
View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://monochrome.tf/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://monochrome.tf/search</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://monochrome.tf/library</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://monochrome.tf/recent</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://monochrome.tf/podcasts</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://monochrome.tf/unreleased</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://monochrome.tf/parties</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://monochrome.tf/donate</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>

View file

@ -1763,6 +1763,10 @@ input[type='search']::-webkit-search-cancel-button {
.settings-tab-content { .settings-tab-content {
display: none; display: none;
max-width: 100%;
min-width: 0;
overflow-x: auto;
overflow-y: visible;
} }
.settings-tab-content.active { .settings-tab-content.active {
@ -1770,14 +1774,6 @@ input[type='search']::-webkit-search-cancel-button {
animation: fade-in-slide-up 0.4s var(--ease-out-back); animation: fade-in-slide-up 0.4s var(--ease-out-back);
} }
/* Prevent settings tab content from overflowing on small displays (fixes #313) */
.settings-tab-content {
max-width: 100%;
min-width: 0;
overflow-x: auto;
overflow-y: visible;
}
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@ -4301,6 +4297,7 @@ input:checked + .slider::before {
justify-content: center; justify-content: center;
gap: 1rem; gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
position: relative;
} }
.fs-volume-btn { .fs-volume-btn {
@ -5910,10 +5907,6 @@ img[src=''] {
color: white; color: white;
} }
#fullscreen-cover-overlay.is-video-mode .fullscreen-volume-container {
margin-top: 0.5rem;
}
#fullscreen-cover-overlay.ui-hidden .fullscreen-main-view, #fullscreen-cover-overlay.ui-hidden .fullscreen-main-view,
#fullscreen-cover-overlay.ui-hidden .fullscreen-controls, #fullscreen-cover-overlay.ui-hidden .fullscreen-controls,
#fullscreen-cover-overlay.ui-hidden #fullscreen-next-track, #fullscreen-cover-overlay.ui-hidden #fullscreen-next-track,
@ -9881,10 +9874,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
.chat-msg { .chat-msg {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
animation: fadeIn 0.2s ease-out; animation: fade-in 0.2s ease-out;
} }
@keyframes fadeIn { @keyframes fade-in {
from { from {
opacity: 0; opacity: 0;
transform: translateY(5px); transform: translateY(5px);