Merge branch 'main' into fix/ui-ux-video-library-fullscreen

This commit is contained in:
edidealt 2026-03-28 11:28:38 +02:00 committed by GitHub
commit 6ace8e19be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 279 additions and 97 deletions

View file

@ -18,48 +18,30 @@ The official Monochrome instance maintained by the core team:
## Community Instances
### Community Monochrome Instances
These instances are community instances of Monochrome & its WebUI:
| Provider | URL | Status |
| ------------- | ---------------------------------------- | --------- |
| **Squid.WTF** | [mono.squid.wtf](https://mono.squid.wtf) | Community |
### UI-Only Instances
These instances provide the tidal-ui web interface, not monochrome:
| Provider | URL | Status |
| ------------- | ------------------------------------------- | --------- |
| **squid.wtf** | [tidal.squid.wtf](https://tidal.squid.wtf) | Community |
| **QQDL** | [tidal.qqdl.site](https://tidal.qqdl.site/) | Community |
---
PLEASE do not use any rehost of monochrome and complain to us about features not working. They are usually out of date, and do not provide the latest features, and accounts are always broken.
## API Instances
Monochrome uses the Hi-Fi API under the hood. Live, up-to-date status trackers (which return JSON) can be found below:
- https://tidal-uptime.jiffy-puffs-1j.workers.dev/
- https://tidal-uptime.props-76styles.workers.dev/
- [https://tidal-uptime.jiffy-puffs-1j.workers.dev](https://tidal-uptime.jiffy-puffs-1j.workers.dev/)
- [https://tidal-uptime.props-76styles.workers.dev](https://tidal-uptime.props-76styles.workers.dev/)
These are available API endpoints that can be used with Monochrome or other Hi-Fi based applications:
### Official & Community APIs
| Provider | URL | Notes |
| ----------------- | ----------------------------------- | ----------------------------------------------------------------------- |
| **Monochrome** | `https://monochrome-api.samidy.com` | Official API |
| | `https://api.monochrome.tf` | Official API |
| | `https://arran.monochrome.tf` | Official API |
| **squid.wtf** | `https://triton.squid.wtf` | Community hosted |
| **Lucida (QQDL)** | `https://wolf.qqdl.site` | Community hosted |
| | `https://maus.qqdl.site` | Community hosted |
| | `https://vogel.qqdl.site` | Community hosted |
| | `https://katze.qqdl.site` | Community hosted |
| | `https://hund.qqdl.site` | Community hosted |
| **Kinoplus** | `https://tidal.kinoplus.online` | Community hosted - [Limited/No-Sub](https://rentry.co/limitedtidalaccs) |
| Provider | URL | Notes |
| ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Monochrome** | `https://monochrome-api.samidy.com` | Official API |
| | `https://api.monochrome.tf` | Official API |
| **geeked.wtf** | `https://hifi.geeked.wtf` | Community hosted - uses the [TypeScript Rewrite](https://github.com/monochrome-music/hifi-api-workers) |
| **Lucida (QQDL)** | `https://wolf.qqdl.site` | Community hosted |
| | `https://maus.qqdl.site` | Community hosted |
| | `https://vogel.qqdl.site` | Community hosted |
| | `https://katze.qqdl.site` | Community hosted |
| | `https://hund.qqdl.site` | Community hosted |
| **Kinoplus** | `https://tidal.kinoplus.online` | Community hosted - [Limited/No-Sub](https://rentry.co/limitedtidalaccs) |
---

View file

@ -72,16 +72,18 @@ class ServerAPI {
}
if (data) {
this.apiInstances = (data.api || []).map((item) => item.url || item);
this.apiInstances = (data.api || [])
.map((item) => item.url || item)
.filter((url) => !/\.squid\.wtf/i.test(url));
return this.apiInstances;
}
console.error('Failed to load instances from all uptime APIs');
return [
'https://hifi.geeked.wtf',
'https://eu-central.monochrome.tf',
'https://us-west.monochrome.tf',
'https://arran.monochrome.tf',
'https://triton.squid.wtf',
'https://api.monochrome.tf',
'https://monochrome-api.samidy.com',
'https://maus.qqdl.site',
@ -135,9 +137,10 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const albumId = params.id;
if (isBot && albumId) {

View file

@ -135,9 +135,10 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const artistId = params.id;
if (isBot && artistId) {

View file

@ -72,7 +72,9 @@ class ServerAPI {
}
if (data) {
this.apiInstances = (data.api || []).map((item) => item.url || item);
this.apiInstances = (data.api || [])
.map((item) => item.url || item)
.filter((url) => !/\.squid\.wtf/i.test(url));
return this.apiInstances;
}
@ -81,7 +83,6 @@ class ServerAPI {
'https://eu-central.monochrome.tf',
'https://us-west.monochrome.tf',
'https://arran.monochrome.tf',
'https://triton.squid.wtf',
'https://api.monochrome.tf',
'https://monochrome-api.samidy.com',
'https://maus.qqdl.site',
@ -135,9 +136,10 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const playlistId = params.id;
if (isBot && playlistId) {

View file

@ -0,0 +1,99 @@
// functions/podcasts/[id].js
const PODCASTINDEX_API_BASE = 'https://api.podcastindex.org/api/1.0';
const PODCAST_API_KEY = 'YU5HMSDYBQQVYDF6QN4P';
const PODCAST_API_SECRET = '8hCvpjSL7T$S7^5ftnf5MhqQwYUYVjM^fmUL3Ld$';
async function sha1(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
async function getAuthHeaders() {
const apiHeaderTime = Math.floor(Date.now() / 1000).toString();
const combined = PODCAST_API_KEY + PODCAST_API_SECRET + apiHeaderTime;
const authHeader = await sha1(combined);
return {
'User-Agent': 'MonochromeMusic/1.0',
'X-Auth-Key': PODCAST_API_KEY,
'X-Auth-Date': apiHeaderTime,
Authorization: authHeader,
};
}
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const podcastId = params.id;
if (isBot && podcastId) {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${PODCASTINDEX_API_BASE}/podcasts/byfeedid?id=${podcastId}&pretty`, {
method: 'GET',
headers,
});
if (!response.ok) throw new Error(`PodcastIndex error: ${response.status}`);
const data = await response.json();
const feed = data.status === 'true' && data.feed ? data.feed : null;
if (feed && feed.title) {
const title = feed.title;
const author = feed.author || feed.ownerName || '';
const episodeCount = feed.episodeCount || 0;
const rawDescription = feed.description || '';
const description = author
? `Podcast by ${author}${episodeCount} Episodes\nListen on Monochrome`
: `Podcast • ${episodeCount} Episodes\nListen on Monochrome`;
const imageUrl = feed.image || feed.artwork || 'https://monochrome.tf/assets/appicon.png';
const pageUrl = new URL(request.url).href;
const metaHtml = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${title}</title>
<meta name="description" content="${description}">
<meta name="theme-color" content="#000000">
<meta property="og:site_name" content="Monochrome">
<meta property="og:title" content="${title}">
<meta property="og:description" content="${description}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:type" content="website">
<meta property="og:url" content="${pageUrl}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${title}">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${imageUrl}">
</head>
<body>
<h1>${title}</h1>
<p>${description}</p>
<img src="${imageUrl}" alt="Podcast Cover">
</body>
</html>
`;
return new Response(metaHtml, { headers: { 'content-type': 'text/html;charset=UTF-8' } });
}
} catch (error) {
console.error(`Error for podcast ${podcastId}:`, error);
}
}
const url = new URL(request.url);
url.pathname = '/';
return env.ASSETS.fetch(new Request(url, request));
}

View file

@ -165,9 +165,10 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const trackId = params.id;
if (isBot && trackId) {

View file

@ -3,9 +3,10 @@
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const username = params.username;
if (isBot && username) {

View file

@ -0,0 +1,116 @@
// functions/userplaylist/[id].js
const POCKETBASE_URL = 'https://data.samidy.xyz';
const PUBLIC_COLLECTION = 'public_playlists';
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot =
/discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot|telegrambot|linkedinbot|mastodon|signal|snapchat|redditbot|skypeuripreview|viberbot|linebot|embedly|quora|outbrain|tumblr|duckduckbot|yandexbot|rogerbot|showyoubot|kakaotalk|naverbot|seznambot|mediapartners|adsbot|petalbot|applebot|ia_archiver/i.test(
userAgent
);
const playlistId = params.id;
if (isBot && playlistId) {
try {
const filter = `uuid="${playlistId}"`;
const apiUrl = `${POCKETBASE_URL}/api/collections/${PUBLIC_COLLECTION}/records?filter=${encodeURIComponent(filter)}&perPage=1`;
const response = await fetch(apiUrl);
if (!response.ok) throw new Error(`PocketBase error: ${response.status}`);
const result = await response.json();
const record = result.items && result.items.length > 0 ? result.items[0] : null;
if (record) {
let extraData = {};
try {
extraData = record.data ? JSON.parse(record.data) : {};
} catch {
extraData = {};
}
const title =
record.title ||
record.name ||
(extraData && (extraData.title || extraData.name)) ||
'Untitled Playlist';
let tracks = [];
try {
tracks = record.tracks ? JSON.parse(record.tracks) : [];
} catch {
tracks = [];
}
const trackCount = tracks.length;
let rawCover = record.image || record.cover || record.playlist_cover || '';
if (!rawCover && extraData && typeof extraData === 'object') {
rawCover = extraData.cover || extraData.image || '';
}
let imageUrl = '';
if (rawCover && (rawCover.startsWith('http') || rawCover.startsWith('data:'))) {
imageUrl = rawCover;
} else if (rawCover) {
imageUrl = `${POCKETBASE_URL}/api/files/${PUBLIC_COLLECTION}/${record.id}/${rawCover}`;
}
if (!imageUrl && tracks.length > 0) {
const firstCover = tracks.find((t) => t.album?.cover)?.album?.cover;
if (firstCover) {
const formattedId = String(firstCover).replace(/-/g, '/');
imageUrl = `https://resources.tidal.com/images/${formattedId}/1080x1080.jpg`;
}
}
if (!imageUrl) {
imageUrl = 'https://monochrome.tf/assets/appicon.png';
}
const description = `Playlist • ${trackCount} Tracks\nListen on Monochrome`;
const pageUrl = new URL(request.url).href;
const metaHtml = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${title}</title>
<meta name="description" content="${description}">
<meta name="theme-color" content="#000000">
<meta property="og:site_name" content="Monochrome">
<meta property="og:title" content="${title}">
<meta property="og:description" content="${description}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:type" content="music.playlist">
<meta property="og:url" content="${pageUrl}">
<meta property="music:song_count" content="${trackCount}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${title}">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${imageUrl}">
</head>
<body>
<h1>${title}</h1>
<p>${description}</p>
<img src="${imageUrl}" alt="Playlist Cover">
</body>
</html>
`;
return new Response(metaHtml, { headers: { 'content-type': 'text/html;charset=UTF-8' } });
}
} catch (error) {
console.error(`Error for user playlist ${playlistId}:`, error);
}
}
const url = new URL(request.url);
url.pathname = '/';
return env.ASSETS.fetch(new Request(url, request));
}

View file

@ -249,7 +249,7 @@ export class ListenBrainzScrobbler {
}
async loveTrack(track) {
if (!track.artist?.name || track.title) return;
if (!track.artist?.name || !track.title) return;
const trackKey = `${track.artist.name}-${track.title}`;
if (!this.isEnabled() || this.lovingTracks.has(trackKey)) return;
this.lovingTracks.add(trackKey);

View file

@ -2731,15 +2731,6 @@ export async function initializeSettings(scrobbler, player, api, ui) {
});
}
const sidebarShowDownloadToggle = document.getElementById('sidebar-show-download-bottom-toggle');
if (sidebarShowDownloadToggle) {
sidebarShowDownloadToggle.checked = sidebarSectionSettings.shouldShowDownload();
sidebarShowDownloadToggle.addEventListener('change', (e) => {
sidebarSectionSettings.setShowDownload(e.target.checked);
sidebarSectionSettings.applySidebarVisibility();
});
}
const sidebarShowDiscordToggle = document.getElementById('sidebar-show-discordbtn-toggle');
if (sidebarShowDiscordToggle) {
sidebarShowDiscordToggle.checked = sidebarSectionSettings.shouldShowDiscord();

View file

@ -77,28 +77,25 @@ export const apiSettings = {
console.error('Failed to load instances from all uptime APIs:', fetchError);
this.defaultInstances = {
api: [
{ url: 'https://eu-central.monochrome.tf', version: '2.4' },
{ url: 'https://us-west.monochrome.tf', version: '2.4' },
{ url: 'https://arran.monochrome.tf', version: '2.4' },
{ url: 'https://triton.squid.wtf', version: '2.4' },
{ url: 'https://api.monochrome.tf', version: '2.3' },
{ url: 'https://hifi.geeked.wtf', version: '2.7' },
{ url: 'https://eu-central.monochrome.tf', version: '2.7' },
{ url: 'https://us-west.monochrome.tf', version: '2.7' },
{ url: 'https://api.monochrome.tf', version: '2.5' },
{ url: 'https://monochrome-api.samidy.com', version: '2.3' },
{ url: 'https://maus.qqdl.site', version: '2.2' },
{ url: 'https://vogel.qqdl.site', version: '2.2' },
{ url: 'https://katze.qqdl.site', version: '2.2' },
{ url: 'https://hund.qqdl.site', version: '2.2' },
{ url: 'https://maus.qqdl.site', version: '2.6' },
{ url: 'https://vogel.qqdl.site', version: '2.6' },
{ url: 'https://katze.qqdl.site', version: '2.6' },
{ url: 'https://hund.qqdl.site', version: '2.6' },
{ url: 'https://tidal.kinoplus.online', version: '2.2' },
{ url: 'https://wolf.qqdl.site', version: '2.2' },
],
streaming: [
{ url: 'https://arran.monochrome.tf', version: '2.4' },
{ url: 'https://triton.squid.wtf', version: '2.4' },
{ url: 'https://maus.qqdl.site', version: '2.2' },
{ url: 'https://vogel.qqdl.site', version: '2.2' },
{ url: 'https://katze.qqdl.site', version: '2.2' },
{ url: 'https://hund.qqdl.site', version: '2.2' },
{ url: 'https://wolf.qqdl.site', version: '2.2' },
{ url: 'https://hifi.p1nkhamster.xyz/', version: '2.6' },
{ url: 'https://hifi.geeked.wtf', version: '2.7' },
{ url: 'https://maus.qqdl.site', version: '2.6' },
{ url: 'https://vogel.qqdl.site', version: '2.6' },
{ url: 'https://katze.qqdl.site', version: '2.6' },
{ url: 'https://hund.qqdl.site', version: '2.6' },
{ url: 'https://wolf.qqdl.site', version: '2.6' },
],
};
this.instancesLoaded = true;
@ -108,12 +105,17 @@ export const apiSettings = {
let groupedInstances = { api: [], streaming: [] };
const isBlockedInstance = (item) => {
const url = typeof item === 'string' ? item : item.url;
return url && /\.squid\.wtf/i.test(url);
};
if (data.api && Array.isArray(data.api)) {
groupedInstances.api = data.api;
groupedInstances.api = data.api.filter((item) => !isBlockedInstance(item));
}
if (data.streaming && Array.isArray(data.streaming)) {
groupedInstances.streaming = data.streaming;
groupedInstances.streaming = data.streaming.filter((item) => !isBlockedInstance(item));
} else if (groupedInstances.api.length > 0) {
groupedInstances.streaming = [...groupedInstances.api];
}
@ -1806,7 +1808,6 @@ export const sidebarSectionSettings = {
SHOW_DONATE_KEY: 'sidebar-show-donate',
SHOW_SETTINGS_KEY: 'sidebar-show-settings',
SHOW_ABOUT_KEY: 'sidebar-show-about',
SHOW_DOWNLOAD_KEY: 'sidebar-show-download',
SHOW_DISCORD_KEY: 'sidebar-show-discord',
SHOW_GITHUB_KEY: 'sidebar-show-github',
ORDER_KEY: 'sidebar-menu-order',
@ -1818,7 +1819,6 @@ export const sidebarSectionSettings = {
'sidebar-nav-donate',
'sidebar-nav-settings',
'sidebar-nav-about-bottom',
'sidebar-nav-download-bottom',
'sidebar-nav-discordbtn',
'sidebar-nav-githubbtn',
],
@ -1919,19 +1919,6 @@ export const sidebarSectionSettings = {
localStorage.setItem(this.SHOW_ABOUT_KEY, enabled ? 'true' : 'false');
},
shouldShowDownload() {
try {
const val = localStorage.getItem(this.SHOW_DOWNLOAD_KEY);
return val === null ? true : val === 'true';
} catch {
return true;
}
},
setShowDownload(enabled) {
localStorage.setItem(this.SHOW_DOWNLOAD_KEY, enabled ? 'true' : 'false');
},
shouldShowDiscord() {
try {
const val = localStorage.getItem(this.SHOW_DISCORD_KEY);
@ -2016,7 +2003,6 @@ export const sidebarSectionSettings = {
{ id: 'sidebar-nav-donate', check: this.shouldShowDonate() },
{ id: 'sidebar-nav-settings', check: this.shouldShowSettings() },
{ id: 'sidebar-nav-about-bottom', check: this.shouldShowAbout() },
{ id: 'sidebar-nav-download-bottom', check: this.shouldShowDownload() },
{ id: 'sidebar-nav-discordbtn', check: this.shouldShowDiscord() },
{ id: 'sidebar-nav-githubbtn', check: this.shouldShowGithub() },
];

View file

@ -107,7 +107,7 @@ function transformErasImages(eras) {
}
async function fetchTrackerData(sheetId) {
const endpoints = ['https://trackerapi-2.artistgrid.cx/get/', 'https://trackerapi-2.artistgrid.cx/get/'];
const endpoints = ['https://trackerapi-1.artistgrid.cx/get/', 'https://trackerapi-2.artistgrid.cx/get/'];
let lastError = null;
for (const baseUrl of endpoints) {