Merge branch 'main' of github.com:monochrome-music/monochrome
This commit is contained in:
commit
9ccc7c748b
14 changed files with 305 additions and 104 deletions
55
.github/workflows/copilot-setup-steps.yml
vendored
55
.github/workflows/copilot-setup-steps.yml
vendored
|
|
@ -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
|
||||
# allow manual testing through the repository's "Actions" tab
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
jobs:
|
||||
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Set the permissions to the lowest permissions possible needed for your steps.
|
||||
# Copilot will be given its own token for its operations.
|
||||
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.
|
||||
# If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
|
||||
contents: read
|
||||
# Set the permissions to the lowest permissions possible needed for your steps.
|
||||
# Copilot will be given its own token for its operations.
|
||||
permissions:
|
||||
contents: read
|
||||
workflows: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Install playwright dependencies
|
||||
run: bun run install:playwright
|
||||
- name: Install playwright dependencies
|
||||
run: bun run install:playwright
|
||||
|
|
|
|||
20
.github/workflows/editors-picks.yml
vendored
20
.github/workflows/editors-picks.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
echo "Files changed in this commit:"
|
||||
echo "$CHANGED"
|
||||
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"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
|
|
@ -37,6 +37,10 @@ jobs:
|
|||
with:
|
||||
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
|
||||
if: steps.backoff.outputs.skip == 'false'
|
||||
run: |
|
||||
|
|
@ -60,9 +64,19 @@ jobs:
|
|||
label = m.group(1).strip()
|
||||
break
|
||||
|
||||
# Copy current picks to archive
|
||||
# Copy current picks to archive, replacing monochrome URLs with UUIDs
|
||||
with open("public/editors-picks.json") as 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:
|
||||
json.dump(current, f, indent=4)
|
||||
|
||||
|
|
@ -89,7 +103,7 @@ jobs:
|
|||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
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 commit -m "chore: update editors picks"
|
||||
git push
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ album:423471869
|
|||
album:250986538
|
||||
album:509761344
|
||||
album:15621057
|
||||
album:103897783
|
||||
album:103897783
|
||||
album:151728406
|
||||
|
|
@ -7,12 +7,17 @@ import re
|
|||
import sys
|
||||
import hashlib
|
||||
import time
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
INPUT_FILE = "editors-picks-input.txt"
|
||||
IMAGES_DIR = "public/editors-picks-images"
|
||||
COUNTRY = "US"
|
||||
|
||||
# Tidal internal token — replace when expired
|
||||
TIDAL_TOKEN = "eyJraWQiOiJ2OU1GbFhqWSIsImFsZyI6IkVTMjU2In0.eyJ0eXBlIjoibzJfYWNjZXNzIiwic2NvcGUiOiIiLCJnVmVyIjowLCJzVmVyIjowLCJjaWQiOjEzNTU3LCJhdCI6IklOVEVSTkFMIiwiZXhwIjoxNzc1MjQ2NzQ2LCJpc3MiOiJodHRwczovL2F1dGgudGlkYWwuY29tL3YxIn0.ksUE4yhQ39IG7oHWk8DyJ91dwIoDVWGzvTAnpeDJ5p-_Gp0F_yO858xDO11AINBaahQCq0jlbqWqIaTqCTOjqg"
|
||||
# Tidal internal token replace when expired
|
||||
TIDAL_TOKEN = "eyJraWQiOiJ2OU1GbFhqWSIsImFsZyI6IkVTMjU2In0.eyJ0eXBlIjoibzJfYWNjZXNzIiwic2NvcGUiOiIiLCJnVmVyIjowLCJzVmVyIjowLCJjaWQiOjEzNTU3LCJhdCI6IklOVEVSTkFMIiwiZXhwIjoxNzc1MzY0MTQwLCJpc3MiOiJodHRwczovL2F1dGgudGlkYWwuY29tL3YxIn0.6ui6itHVQ-OXPF0F9mbf5KcKz1fKYJNsa1vBAj60upXpcN-DQG8JPKBlqJN6RuBEH8yhwYj2wh4YJ-TOOuO8DA"
|
||||
|
||||
TIDAL_HEADERS = {
|
||||
"accept": "*/*",
|
||||
|
|
@ -83,6 +88,61 @@ def fetch_podcast(feed_id):
|
|||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
def transform_album(d):
|
||||
|
|
@ -95,7 +155,7 @@ def transform_album(d):
|
|||
"name": d.get("artist", {}).get("name"),
|
||||
},
|
||||
"releaseDate": d.get("releaseDate"),
|
||||
"cover": d.get("cover"),
|
||||
"cover": process_cover(d.get("cover")),
|
||||
"explicit": d.get("explicit"),
|
||||
"audioQuality": d.get("audioQuality"),
|
||||
"mediaMetadata": d.get("mediaMetadata"),
|
||||
|
|
@ -107,7 +167,7 @@ def transform_artist(d):
|
|||
"type": "artist",
|
||||
"id": d.get("id"),
|
||||
"name": d.get("name"),
|
||||
"picture": d.get("picture"),
|
||||
"picture": process_cover(d.get("picture")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -124,7 +184,7 @@ def transform_track(d):
|
|||
"album": {
|
||||
"id": album.get("id"),
|
||||
"title": album.get("title"),
|
||||
"cover": album.get("cover"),
|
||||
"cover": process_cover(album.get("cover")),
|
||||
},
|
||||
"duration": d.get("duration"),
|
||||
"explicit": d.get("explicit"),
|
||||
|
|
@ -140,7 +200,7 @@ def transform_playlist(d):
|
|||
"type": "playlist",
|
||||
"id": d.get("uuid"),
|
||||
"title": d.get("title"),
|
||||
"cover": cover,
|
||||
"cover": process_cover(cover),
|
||||
"numberOfTracks": d.get("numberOfTracks", 0),
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +213,7 @@ def transform_userplaylist(d):
|
|||
"type": "user-playlist",
|
||||
"id": d.get("uuid"),
|
||||
"name": d.get("title"),
|
||||
"cover": cover,
|
||||
"cover": process_cover(cover),
|
||||
"numberOfTracks": d.get("numberOfTracks", 0),
|
||||
"username": creator.get("name"),
|
||||
}
|
||||
|
|
@ -198,6 +258,8 @@ def read_items(path):
|
|||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
clear_images_dir()
|
||||
|
||||
FETCHERS = {
|
||||
"album": (fetch_album, transform_album),
|
||||
"artist": (fetch_artist, transform_artist),
|
||||
|
|
@ -212,7 +274,7 @@ picks = []
|
|||
|
||||
for item_type, item_id in items:
|
||||
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
|
||||
fetch_fn, transform_fn = FETCHERS[item_type]
|
||||
data = fetch_fn(item_id)
|
||||
|
|
|
|||
67
index.html
67
index.html
|
|
@ -3,24 +3,79 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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="alternate" hreflang="en" href="https://monochrome.tf/" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<base href="/" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<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 -->
|
||||
<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://cdn.jsdelivr.net" crossorigin />
|
||||
<link rel="preconnect" href="https://unpkg.com" crossorigin />
|
||||
|
||||
<link rel="apple-touch-icon" href="/assets/logo.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
|
|
|||
50
js/app.js
50
js/app.js
|
|
@ -127,32 +127,36 @@ async function loadDownloadsModule() {
|
|||
}
|
||||
|
||||
async function fetchcontributors() {
|
||||
const response = await fetch('https://api.samidy.com/api/contributors');
|
||||
const data1 = await response.json();
|
||||
try {
|
||||
const response = await fetch('https://api.samidy.com/api/contributors');
|
||||
if (!response.ok) return;
|
||||
const data1 = await response.json();
|
||||
|
||||
const data = data1.filter(
|
||||
(user) => user.type !== 'Bot' && user.login !== 'edidealt' && user.login !== 'satanyahoo'
|
||||
);
|
||||
const data = data1.filter(
|
||||
(user) => user.type !== 'Bot' && user.login !== 'edidealt' && user.login !== 'satanyahoo'
|
||||
);
|
||||
|
||||
const edideaur = data.find((user) => user.login === 'edideaur');
|
||||
if (edideaur) {
|
||||
edideaur.contributions += data1.find((u) => u.login === 'edidealt')?.contributions || 0;
|
||||
edideaur.contributions += data1.find((u) => u.login === 'satanyahoo')?.contributions || 0;
|
||||
}
|
||||
const edideaur = data.find((user) => user.login === 'edideaur');
|
||||
if (edideaur) {
|
||||
edideaur.contributions += data1.find((u) => u.login === 'edidealt')?.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) => {
|
||||
const userDIV = document.createElement('div');
|
||||
userDIV.innerHTML = `
|
||||
<a href="${user.html_url}" target="_blank">
|
||||
<img src="${user.avatar_url}" alt="${user.login}" width="50" style="border-radius: 50%;">
|
||||
<span>${user.login}</span>
|
||||
<span class="contrib">Contributions: ${user.contributions}</span>
|
||||
</a>
|
||||
`;
|
||||
con.appendChild(userDIV);
|
||||
});
|
||||
data.forEach((user) => {
|
||||
const userDIV = document.createElement('div');
|
||||
userDIV.innerHTML = `
|
||||
<a href="${user.html_url}" target="_blank">
|
||||
<img src="${user.avatar_url}" alt="${user.login}" width="50" style="border-radius: 50%;">
|
||||
<span>${user.login}</span>
|
||||
<span class="contrib">Contributions: ${user.contributions}</span>
|
||||
</a>
|
||||
`;
|
||||
con.appendChild(userDIV);
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function loadMetadataModule() {
|
||||
|
|
@ -500,7 +504,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
// Initialize tracker
|
||||
initTracker().catch(console.error);
|
||||
|
||||
fetchcontributors();
|
||||
await fetchcontributors();
|
||||
const castBtn = document.getElementById('cast-btn');
|
||||
initializeCasting(audioPlayer, castBtn);
|
||||
|
||||
|
|
|
|||
|
|
@ -278,13 +278,13 @@ export class LyricsManager {
|
|||
|
||||
// Load Kuroshiro from CDN
|
||||
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
|
||||
if (!window.KuromojiAnalyzer) {
|
||||
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'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
55
js/player.js
55
js/player.js
|
|
@ -487,7 +487,7 @@ export class Player {
|
|||
const timeRemaining = duration - currentTime;
|
||||
|
||||
// Preload if we are in last 30 seconds of song
|
||||
const shouldPreload = (duration > 0 && timeRemaining <= 30);
|
||||
const shouldPreload = duration > 0 && timeRemaining <= 30;
|
||||
|
||||
if (shouldPreload) {
|
||||
this._pendingPreload = false;
|
||||
|
|
@ -537,7 +537,7 @@ export class Player {
|
|||
albumPeakAmplitude: trackData.info.albumPeakAmplitude,
|
||||
};
|
||||
}
|
||||
} catch(e) {} // Fail silently
|
||||
} catch (_e) {} // Fail silently
|
||||
}
|
||||
|
||||
this.preloadCache.set(track.id, streamInfo);
|
||||
|
|
@ -546,30 +546,45 @@ export class Player {
|
|||
// Warm connection and pre-fetch
|
||||
if (!streamUrl.startsWith('blob:')) {
|
||||
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 {
|
||||
let preloadConfig = undefined;
|
||||
if (typeof this.shakaPlayer.getConfiguration === 'function') {
|
||||
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) {
|
||||
preloadConfig.abr.defaultBandwidthEstimate = stats.estimatedBandwidth;
|
||||
}
|
||||
|
||||
|
||||
// Lock the preload to the exact current audio codec to prevent ABR mismatch,
|
||||
// which forces the player to discard and re-fetch chunks on slow connections.
|
||||
preloadConfig.abr.enabled = false;
|
||||
try {
|
||||
const variants = typeof this.shakaPlayer.getVariantTracks === 'function' ? this.shakaPlayer.getVariantTracks() : [];
|
||||
const activeVariant = variants.find(v => v.active);
|
||||
const variants =
|
||||
typeof this.shakaPlayer.getVariantTracks === 'function'
|
||||
? this.shakaPlayer.getVariantTracks()
|
||||
: [];
|
||||
const activeVariant = variants.find((v) => v.active);
|
||||
if (activeVariant && 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;
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore preload errors, will just load fresh
|
||||
}
|
||||
} else {
|
||||
|
|
@ -801,7 +816,7 @@ export class Player {
|
|||
this.hls.destroy();
|
||||
this.hls = null;
|
||||
}
|
||||
|
||||
|
||||
// Retain the initialized Shaka player if we are remaining on the same HTMLMediaElement
|
||||
if (this.shakaInitialized && this.shakaPlayer) {
|
||||
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
||||
|
|
@ -1015,14 +1030,17 @@ export class Player {
|
|||
} else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) {
|
||||
await this.shakaPlayer.attach(activeElement);
|
||||
|
||||
const loadTarget = track.type == 'video' && this.preloadCache.has(track.id) ?
|
||||
(this.preloadCache.get(track.id).preloadManager || streamUrl) : streamUrl;
|
||||
const loadTarget =
|
||||
track.type == 'video' && this.preloadCache.has(track.id)
|
||||
? this.preloadCache.get(track.id).preloadManager || streamUrl
|
||||
: streamUrl;
|
||||
|
||||
try {
|
||||
await this.shakaPlayer.load(loadTarget);
|
||||
} catch (e) {
|
||||
console.error("PreloadManager load Error:", e); if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
|
||||
else throw e;
|
||||
console.error('PreloadManager load Error:', e);
|
||||
if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
|
||||
else throw e;
|
||||
}
|
||||
|
||||
this.shakaInitialized = true;
|
||||
|
|
@ -1097,8 +1115,9 @@ export class Player {
|
|||
await this.shakaPlayer.load(loadTarget);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("PreloadManager load Error:", e); if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
|
||||
else throw e;
|
||||
console.error('PreloadManager load Error:', e);
|
||||
if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
|
||||
else throw e;
|
||||
}
|
||||
|
||||
this.shakaInitialized = true;
|
||||
|
|
@ -1109,7 +1128,7 @@ export class Player {
|
|||
|
||||
this.updateAdaptiveQualityBadge();
|
||||
|
||||
// Instantly trigger playback rather than explicitly waiting for 'canplay'
|
||||
// Instantly trigger playback rather than explicitly waiting for 'canplay'
|
||||
// which delays the event loop and natively adds gap/latency
|
||||
await this.safePlay(activeElement);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
|
|||
|
||||
if (explicit !== undefined) {
|
||||
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();
|
||||
mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0));
|
||||
} else {
|
||||
|
|
|
|||
BIN
public/assets/banner-twitter.jpg
Normal file
BIN
public/assets/banner-twitter.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/assets/banner.jpg
Normal file
BIN
public/assets/banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
11
public/robots.txt
Normal file
11
public/robots.txt
Normal 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
43
public/sitemap.xml
Normal 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>
|
||||
21
styles.css
21
styles.css
|
|
@ -1763,6 +1763,10 @@ input[type='search']::-webkit-search-cancel-button {
|
|||
|
||||
.settings-tab-content {
|
||||
display: none;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
|
|
@ -4301,6 +4297,7 @@ input:checked + .slider::before {
|
|||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fs-volume-btn {
|
||||
|
|
@ -5910,10 +5907,6 @@ img[src=''] {
|
|||
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-controls,
|
||||
#fullscreen-cover-overlay.ui-hidden #fullscreen-next-track,
|
||||
|
|
@ -9881,10 +9874,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
|||
|
||||
.chat-msg {
|
||||
margin-bottom: 0.5rem;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
|
|
|
|||
Loading…
Reference in a new issue