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
|
# 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
|
||||||
|
|
|
||||||
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 "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
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,5 @@ album:423471869
|
||||||
album:250986538
|
album:250986538
|
||||||
album:509761344
|
album:509761344
|
||||||
album:15621057
|
album:15621057
|
||||||
album:103897783
|
album:103897783
|
||||||
|
album:151728406
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
67
index.html
67
index.html
|
|
@ -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" />
|
||||||
|
|
|
||||||
50
js/app.js
50
js/app.js
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
55
js/player.js
55
js/player.js
|
|
@ -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,30 +546,45 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock the preload to the exact current audio codec to prevent ABR mismatch,
|
// 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.
|
// 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 {
|
||||||
|
|
@ -801,7 +816,7 @@ export class Player {
|
||||||
this.hls.destroy();
|
this.hls.destroy();
|
||||||
this.hls = null;
|
this.hls = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retain the initialized Shaka player if we are remaining on the same HTMLMediaElement
|
// Retain the initialized Shaka player if we are remaining on the same HTMLMediaElement
|
||||||
if (this.shakaInitialized && this.shakaPlayer) {
|
if (this.shakaInitialized && this.shakaPlayer) {
|
||||||
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1109,7 +1128,7 @@ export class Player {
|
||||||
|
|
||||||
this.updateAdaptiveQualityBadge();
|
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
|
// which delays the event loop and natively adds gap/latency
|
||||||
await this.safePlay(activeElement);
|
await this.safePlay(activeElement);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
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 {
|
.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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue