Home page change-up, shuffle like, self-host + improvements & fixes
This commit is contained in:
parent
8eb8012cfc
commit
a1498548a8
25 changed files with 627 additions and 1932 deletions
|
|
@ -8,6 +8,8 @@ API:
|
|||
| | https://vogel.qqdl.site |
|
||||
| | https://katze.qqdl.site |
|
||||
| | https://hund.qqdl.site |
|
||||
| Spotisaver | https://hifi-one.spotisaver.net |
|
||||
| | https://hifi-two.spotisaver.net |
|
||||
| Kinoplus | https://tidal.kinoplus.online |
|
||||
| Binimum | https://tidal-api.binimum.org |
|
||||
|
||||
|
|
|
|||
106
index.html
106
index.html
|
|
@ -305,6 +305,33 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-db-modal" class="modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="margin: 0;">Custom Database/Auth</h3>
|
||||
<button id="custom-db-reset" class="btn-secondary danger" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;">Reset to Defaults</button>
|
||||
</div>
|
||||
<p style="font-size: 0.9rem; color: var(--muted-foreground); margin-bottom: 1rem;">
|
||||
Configure custom PocketBase and Firebase instances. Leave empty to use defaults.
|
||||
<br>
|
||||
A Guide To Set This Up Can Be Found <a href="https://github.com/SamidyFR/monochrome/blob/main/self-hosted-database.md" style="text-decoration: underline;">Here</a>.
|
||||
</p>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">PocketBase URL</label>
|
||||
<input type="url" id="custom-pb-url" class="template-input" placeholder="https://monodb.samidy.com">
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Firebase Configuration (JSON)</label>
|
||||
<textarea id="custom-firebase-config" class="template-input" style="height: 150px; font-family: monospace; font-size: 0.8rem; resize: vertical;" placeholder='{"apiKey": "...", ...}'></textarea>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="custom-db-cancel" class="btn-secondary">Cancel</button>
|
||||
<button id="custom-db-save" class="btn-primary">Save & Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebar-overlay"></div>
|
||||
|
||||
<div id="csv-import-progress" class="csv-import-progress" style="display: none">
|
||||
|
|
@ -590,18 +617,49 @@
|
|||
</header>
|
||||
|
||||
<div id="page-home" class="page">
|
||||
<section class="content-section">
|
||||
<h2 class="section-title">Recent Albums</h2>
|
||||
<div class="card-grid" id="home-recent-albums"></div>
|
||||
</section>
|
||||
<section class="content-section">
|
||||
<h2 class="section-title">Recent Artists</h2>
|
||||
<div class="card-grid" id="home-recent-artists"></div>
|
||||
</section>
|
||||
<section class="content-section">
|
||||
<h2 class="section-title">Recent Playlists and Mixes</h2>
|
||||
<div class="card-grid" id="home-recent-playlists"></div>
|
||||
</section>
|
||||
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem;">
|
||||
<h2 style="margin-bottom: 1rem;">Welcome to Monochrome</h2>
|
||||
<p style="color: var(--muted-foreground);">You haven't listened to anything yet. Search for your favorite songs to get started!</p>
|
||||
</div>
|
||||
|
||||
<div id="home-content" style="display: none;">
|
||||
<section class="content-section">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<h2 class="section-title" style="margin-bottom: 0;">Recommended Songs</h2>
|
||||
<button class="btn-secondary" id="refresh-songs-btn" title="Refresh" style="padding: 4px 8px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="track-list" id="home-recommended-songs"></div>
|
||||
</section>
|
||||
<section class="content-section">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<h2 class="section-title" style="margin-bottom: 0;">Recommended Albums</h2>
|
||||
<button class="btn-secondary" id="refresh-albums-btn" title="Refresh" style="padding: 4px 8px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-grid" id="home-recommended-albums"></div>
|
||||
</section>
|
||||
<section class="content-section">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<h2 class="section-title" style="margin-bottom: 0;">Recommended Artists</h2>
|
||||
<button class="btn-secondary" id="refresh-artists-btn" title="Refresh" style="padding: 4px 8px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-grid" id="home-recommended-artists"></div>
|
||||
</section>
|
||||
<section class="content-section">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<h2 class="section-title" style="margin-bottom: 0;">Jump Back In</h2>
|
||||
<button class="btn-secondary" id="clear-recent-btn" title="Clear History" style="padding: 4px 8px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-grid" id="home-recent-mixed"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-search" class="page">
|
||||
|
|
@ -647,6 +705,11 @@
|
|||
<button class="search-tab" data-tab="local">Local Files</button>
|
||||
</div>
|
||||
<div class="search-tab-content active" id="library-tab-tracks">
|
||||
<div style="display: flex; justify-content: flex-start; margin-bottom: 0.5rem;">
|
||||
<button id="shuffle-liked-tracks-btn" class="btn-secondary" style="display: none; width: 32px; height: 32px; padding: 0; align-items: center; justify-content: center; border-radius: 50%;" title="Shuffle Liked Tracks">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 14 4 4-4 4"/><path d="m18 2 4 4-4 4"/><path d="M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22"/><path d="M2 6h1.972a4 4 0 0 1 3.6 2.2"/><path d="M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="track-list" id="library-tracks-container"></div>
|
||||
</div>
|
||||
<div class="search-tab-content" id="library-tab-albums">
|
||||
|
|
@ -1310,6 +1373,13 @@
|
|||
<input type="file" id="import-library-input" style="display: none" accept=".json" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">ADVANCED: Custom Database/Auth</span>
|
||||
<span class="description">Configure custom PocketBase and Firebase instances</span>
|
||||
</div>
|
||||
<button id="custom-db-btn" class="btn-secondary">Configure</button>
|
||||
</div>
|
||||
<div id="api-instance-manager">
|
||||
<div class="setting-item" style="padding-bottom: 1rem; border: none">
|
||||
<div class="info">
|
||||
|
|
@ -1528,6 +1598,18 @@
|
|||
All data is anonymous. We do not store anything like emails, usernames, or anything
|
||||
sensitive. <br />
|
||||
</p>
|
||||
<p style="padding-top: 50px; text-align: center; color: #8b8b93">
|
||||
However, if you want complete control over your data, we allow you to use your own Database Configuration.
|
||||
</p>
|
||||
<div style="
|
||||
display: flex;
|
||||
gap: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 25px;
|
||||
">
|
||||
<a id="advanced-config-link" class="btn-secondary" href="#settings">Advanced: Custom Configuration</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="page-donate" class="page">
|
||||
|
|
|
|||
21
js/app.js
21
js/app.js
|
|
@ -812,6 +812,27 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (e.target.closest('#shuffle-liked-tracks-btn')) {
|
||||
const btn = e.target.closest('#shuffle-liked-tracks-btn');
|
||||
if (btn.disabled) return;
|
||||
|
||||
try {
|
||||
const likedTracks = await db.getFavorites('track');
|
||||
if (likedTracks.length > 0) {
|
||||
// Shuffle array
|
||||
for (let i = likedTracks.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[likedTracks[i], likedTracks[j]] = [likedTracks[j], likedTracks[i]];
|
||||
}
|
||||
player.setQueue(likedTracks, 0);
|
||||
document.getElementById('shuffle-btn').classList.remove('active');
|
||||
player.playTrackFromQueue();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to shuffle liked tracks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target.closest('#download-discography-btn')) {
|
||||
const btn = e.target.closest('#download-discography-btn');
|
||||
if (btn.disabled) return;
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign
|
|||
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
||||
};
|
||||
|
||||
if (enrichedTrack.album && !enrichedTrack.album.title && enrichedTrack.album.id) {
|
||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
||||
try {
|
||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||
if (albumData.album) {
|
||||
|
|
@ -630,7 +630,7 @@ export async function downloadTrackWithMetadata(track, quality, api, lyricsManag
|
|||
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
||||
};
|
||||
|
||||
if (enrichedTrack.album && !enrichedTrack.album.title && enrichedTrack.album.id) {
|
||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
||||
try {
|
||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||
if (albumData.album) {
|
||||
|
|
|
|||
87
js/events.js
87
js/events.js
|
|
@ -1,5 +1,5 @@
|
|||
//js/events.js
|
||||
import { SVG_PLAY, SVG_PAUSE, SVG_VOLUME, SVG_MUTE, REPEAT_MODE, trackDataStore, formatTime } from './utils.js';
|
||||
import { SVG_PLAY, SVG_PAUSE, SVG_VOLUME, SVG_MUTE, REPEAT_MODE, trackDataStore, formatTime, SVG_BIN, escapeHtml } from './utils.js';
|
||||
import { lastFMStorage, waveformSettings } from './storage.js';
|
||||
import { showNotification, downloadTrackWithMetadata } from './downloads.js';
|
||||
import { downloadQualitySettings } from './storage.js';
|
||||
|
|
@ -22,6 +22,10 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
|||
// History tracking
|
||||
let historyLoggedTrackId = null;
|
||||
|
||||
audioPlayer.addEventListener('loadstart', () => {
|
||||
historyLoggedTrackId = null;
|
||||
});
|
||||
|
||||
// Sync UI with player state on load
|
||||
if (player.shuffleActive) {
|
||||
shuffleBtn.classList.add('active');
|
||||
|
|
@ -652,41 +656,46 @@ export async function handleTrackAction(
|
|||
}
|
||||
}
|
||||
} else if (action === 'add-to-playlist') {
|
||||
const playlists = await db.getPlaylists(true);
|
||||
if (playlists.length === 0) {
|
||||
showNotification('No playlists yet. Create one first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('playlist-select-modal');
|
||||
const list = document.getElementById('playlist-select-list');
|
||||
const cancelBtn = document.getElementById('playlist-select-cancel');
|
||||
const overlay = modal.querySelector('.modal-overlay');
|
||||
|
||||
// Check what playlists already have this
|
||||
const trackId = item.id;
|
||||
const playlistsWithTrack = new Set();
|
||||
|
||||
for (const playlist of playlists) {
|
||||
if (playlist.tracks && playlist.tracks.some((track) => track.id == trackId)) {
|
||||
playlistsWithTrack.add(playlist.id);
|
||||
const renderModal = async () => {
|
||||
const playlists = await db.getPlaylists(true);
|
||||
if (playlists.length === 0) {
|
||||
showNotification('No playlists yet. Create one first.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const checkmarkSvg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
||||
const trackId = item.id;
|
||||
const playlistsWithTrack = new Set();
|
||||
|
||||
list.innerHTML = playlists
|
||||
.map((p) => {
|
||||
const alreadyContains = playlistsWithTrack.has(p.id);
|
||||
return `
|
||||
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
|
||||
<span>${p.name}</span>
|
||||
${alreadyContains ? `<span class="checkmark">${checkmarkSvg}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
for (const playlist of playlists) {
|
||||
if (playlist.tracks && playlist.tracks.some((track) => track.id == trackId)) {
|
||||
playlistsWithTrack.add(playlist.id);
|
||||
}
|
||||
}
|
||||
|
||||
list.innerHTML = playlists
|
||||
.map((p) => {
|
||||
const alreadyContains = playlistsWithTrack.has(p.id);
|
||||
return `
|
||||
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
|
||||
<span>${p.name}</span>
|
||||
${
|
||||
alreadyContains
|
||||
? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!(await renderModal())) return;
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
|
|
@ -694,15 +703,23 @@ export async function handleTrackAction(
|
|||
};
|
||||
|
||||
const handleOptionClick = async (e) => {
|
||||
const removeBtn = e.target.closest('.remove-from-playlist-btn-modal');
|
||||
const option = e.target.closest('.modal-option');
|
||||
if (option) {
|
||||
const playlistId = option.dataset.id;
|
||||
const alreadyContains = playlistsWithTrack.has(playlistId);
|
||||
|
||||
if (alreadyContains) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!option) return;
|
||||
|
||||
const playlistId = option.dataset.id;
|
||||
|
||||
if (removeBtn) {
|
||||
e.stopPropagation();
|
||||
await db.removeTrackFromPlaylist(playlistId, item.id);
|
||||
const updatedPlaylist = await db.getPlaylist(playlistId);
|
||||
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
||||
showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`);
|
||||
await renderModal();
|
||||
} else {
|
||||
if (option.classList.contains('already-contains')) return;
|
||||
|
||||
await db.addTrackToPlaylist(playlistId, item);
|
||||
const updatedPlaylist = await db.getPlaylist(playlistId);
|
||||
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
||||
|
|
|
|||
|
|
@ -392,8 +392,9 @@ function createVorbisCommentBlock(track) {
|
|||
if (track.album?.title) {
|
||||
comments.push(['ALBUM', track.album.title]);
|
||||
}
|
||||
if (track.album?.artist?.name) {
|
||||
comments.push(['ALBUMARTIST', track.album.artist.name]);
|
||||
const albumArtist = track.album?.artist?.name || track.artist?.name;
|
||||
if (albumArtist) {
|
||||
comments.push(['ALBUMARTIST', albumArtist]);
|
||||
}
|
||||
if (track.trackNumber) {
|
||||
comments.push(['TRACKNUMBER', String(track.trackNumber)]);
|
||||
|
|
@ -744,7 +745,7 @@ function createMp4MetadataAtoms(track) {
|
|||
'©nam': track.title || DEFAULT_TITLE,
|
||||
'©ART': track.artist?.name || DEFAULT_ARTIST,
|
||||
'©alb': track.album?.title || DEFAULT_ALBUM,
|
||||
aART: track.album?.artist?.name || DEFAULT_ARTIST,
|
||||
aART: track.album?.artist?.name || track.artist?.name || DEFAULT_ARTIST,
|
||||
};
|
||||
|
||||
if (track.trackNumber) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { db } from './db.js';
|
||||
import { authManager } from './accounts/auth.js';
|
||||
import { syncManager } from './accounts/pocketbase.js';
|
||||
import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js';
|
||||
|
||||
export function initializeSettings(scrobbler, player, api, ui) {
|
||||
// Initialize account system UI & Settings
|
||||
|
|
@ -537,4 +538,74 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
const customDbBtn = document.getElementById('custom-db-btn');
|
||||
const customDbModal = document.getElementById('custom-db-modal');
|
||||
const customPbUrlInput = document.getElementById('custom-pb-url');
|
||||
const customFirebaseConfigInput = document.getElementById('custom-firebase-config');
|
||||
const customDbSaveBtn = document.getElementById('custom-db-save');
|
||||
const customDbResetBtn = document.getElementById('custom-db-reset');
|
||||
const customDbCancelBtn = document.getElementById('custom-db-cancel');
|
||||
|
||||
if (customDbBtn && customDbModal) {
|
||||
customDbBtn.addEventListener('click', () => {
|
||||
const pbUrl = localStorage.getItem('monochrome-pocketbase-url') || '';
|
||||
const fbConfig = localStorage.getItem('monochrome-firebase-config');
|
||||
|
||||
customPbUrlInput.value = pbUrl;
|
||||
if (fbConfig) {
|
||||
try {
|
||||
customFirebaseConfigInput.value = JSON.stringify(JSON.parse(fbConfig), null, 2);
|
||||
} catch {
|
||||
customFirebaseConfigInput.value = fbConfig;
|
||||
}
|
||||
} else {
|
||||
customFirebaseConfigInput.value = '';
|
||||
}
|
||||
|
||||
customDbModal.classList.add('active');
|
||||
});
|
||||
|
||||
const closeCustomDbModal = () => {
|
||||
customDbModal.classList.remove('active');
|
||||
};
|
||||
|
||||
customDbCancelBtn.addEventListener('click', closeCustomDbModal);
|
||||
customDbModal.querySelector('.modal-overlay').addEventListener('click', closeCustomDbModal);
|
||||
|
||||
customDbSaveBtn.addEventListener('click', () => {
|
||||
const pbUrl = customPbUrlInput.value.trim();
|
||||
const fbConfigStr = customFirebaseConfigInput.value.trim();
|
||||
|
||||
if (pbUrl) {
|
||||
localStorage.setItem('monochrome-pocketbase-url', pbUrl);
|
||||
} else {
|
||||
localStorage.removeItem('monochrome-pocketbase-url');
|
||||
}
|
||||
|
||||
if (fbConfigStr) {
|
||||
try {
|
||||
const fbConfig = JSON.parse(fbConfigStr);
|
||||
saveFirebaseConfig(fbConfig);
|
||||
} catch (e) {
|
||||
alert('Invalid JSON for Firebase Config');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clearFirebaseConfig();
|
||||
}
|
||||
|
||||
alert('Settings saved. Reloading...');
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
customDbResetBtn.addEventListener('click', () => {
|
||||
if (confirm('Reset custom database settings to default?')) {
|
||||
localStorage.removeItem('monochrome-pocketbase-url');
|
||||
clearFirebaseConfig();
|
||||
alert('Settings reset. Reloading...');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//storage.js
|
||||
export const apiSettings = {
|
||||
STORAGE_KEY: 'monochrome-api-instances-v2',
|
||||
INSTANCES_URL: '../public/instances.json',
|
||||
INSTANCES_URL: 'instances.json',
|
||||
SPEED_TEST_CACHE_KEY: 'monochrome-instance-speeds',
|
||||
SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60,
|
||||
defaultInstances: { api: [], streaming: [] },
|
||||
|
|
@ -171,6 +171,17 @@ export const apiSettings = {
|
|||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (stored) {
|
||||
instancesObj = JSON.parse(stored);
|
||||
|
||||
// love it when local storage doesnt update
|
||||
if (instancesObj?.api?.length === 2) {
|
||||
const hasBinimum = instancesObj.api.some((url) => url.includes('tidal-api.binimum.org'));
|
||||
const hasSamidy = instancesObj.api.some((url) => url.includes('monochrome-api.samidy.com'));
|
||||
|
||||
if (hasBinimum && hasSamidy) {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
instancesObj = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!instancesObj) {
|
||||
|
|
@ -276,6 +287,10 @@ export const recentActivityManager = {
|
|||
this._save(data);
|
||||
},
|
||||
|
||||
clear() {
|
||||
this._save({ artists: [], albums: [], playlists: [], mixes: [] });
|
||||
},
|
||||
|
||||
addArtist(artist) {
|
||||
this._add('artists', artist);
|
||||
},
|
||||
|
|
|
|||
298
js/ui.js
298
js/ui.js
|
|
@ -708,9 +708,13 @@ export class UIRenderer {
|
|||
const localContainer = document.getElementById('library-local-container');
|
||||
|
||||
const likedTracks = await db.getFavorites('track');
|
||||
const shuffleBtn = document.getElementById('shuffle-liked-tracks-btn');
|
||||
|
||||
if (likedTracks.length) {
|
||||
if (shuffleBtn) shuffleBtn.style.display = 'flex';
|
||||
this.renderListWithTracks(tracksContainer, likedTracks, true);
|
||||
} else {
|
||||
if (shuffleBtn) shuffleBtn.style.display = 'none';
|
||||
tracksContainer.innerHTML = createPlaceholder('No liked tracks yet.');
|
||||
}
|
||||
|
||||
|
|
@ -832,87 +836,249 @@ export class UIRenderer {
|
|||
|
||||
async renderHomePage() {
|
||||
this.showPage('home');
|
||||
const recents = recentActivityManager.getRecents();
|
||||
|
||||
const albumsContainer = document.getElementById('home-recent-albums');
|
||||
const artistsContainer = document.getElementById('home-recent-artists');
|
||||
const playlistsContainer = document.getElementById('home-recent-playlists');
|
||||
|
||||
if (recents.albums.length) {
|
||||
albumsContainer.innerHTML = recents.albums.map((album) => this.createAlbumCardHTML(album)).join('');
|
||||
recents.albums.forEach((album) => {
|
||||
const el = albumsContainer.querySelector(`[data-album-id="${album.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, album);
|
||||
this.updateLikeState(el, 'album', album.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
albumsContainer.innerHTML = createPlaceholder("You haven't viewed any albums yet.");
|
||||
|
||||
const welcomeEl = document.getElementById('home-welcome');
|
||||
const contentEl = document.getElementById('home-content');
|
||||
|
||||
const history = await db.getHistory();
|
||||
const favorites = await db.getFavorites('track');
|
||||
const playlists = await db.getPlaylists(true);
|
||||
|
||||
if (history.length === 0 && favorites.length === 0 && playlists.length === 0) {
|
||||
if (welcomeEl) welcomeEl.style.display = 'block';
|
||||
if (contentEl) contentEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (recents.artists.length) {
|
||||
artistsContainer.innerHTML = recents.artists.map((artist) => this.createArtistCardHTML(artist)).join('');
|
||||
recents.artists.forEach((artist) => {
|
||||
const el = artistsContainer.querySelector(`[data-artist-id="${artist.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, artist);
|
||||
this.updateLikeState(el, 'artist', artist.id);
|
||||
if (welcomeEl) welcomeEl.style.display = 'none';
|
||||
if (contentEl) contentEl.style.display = 'block';
|
||||
|
||||
const refreshSongsBtn = document.getElementById('refresh-songs-btn');
|
||||
const refreshAlbumsBtn = document.getElementById('refresh-albums-btn');
|
||||
const refreshArtistsBtn = document.getElementById('refresh-artists-btn');
|
||||
const clearRecentBtn = document.getElementById('clear-recent-btn');
|
||||
|
||||
if (refreshSongsBtn) refreshSongsBtn.onclick = () => this.renderHomeSongs(true);
|
||||
if (refreshAlbumsBtn) refreshAlbumsBtn.onclick = () => this.renderHomeAlbums(true);
|
||||
if (refreshArtistsBtn) refreshArtistsBtn.onclick = () => this.renderHomeArtists(true);
|
||||
if (clearRecentBtn) clearRecentBtn.onclick = () => {
|
||||
if (confirm('Clear recent activity?')) {
|
||||
recentActivityManager.clear();
|
||||
this.renderHomeRecent();
|
||||
}
|
||||
};
|
||||
|
||||
this.renderHomeSongs();
|
||||
this.renderHomeAlbums();
|
||||
this.renderHomeArtists();
|
||||
this.renderHomeRecent();
|
||||
}
|
||||
|
||||
async getSeeds() {
|
||||
const history = await db.getHistory();
|
||||
const favorites = await db.getFavorites('track');
|
||||
const playlists = await db.getPlaylists(true);
|
||||
const playlistTracks = playlists.flatMap(p => p.tracks || []);
|
||||
|
||||
// Prioritize: Playlists > Favorites > History
|
||||
// Take random samples from each to form seeds
|
||||
const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
|
||||
|
||||
const seeds = [
|
||||
...shuffle(playlistTracks).slice(0, 20),
|
||||
...shuffle(favorites).slice(0, 20),
|
||||
...shuffle(history).slice(0, 10)
|
||||
];
|
||||
|
||||
return shuffle(seeds);
|
||||
}
|
||||
|
||||
async renderHomeSongs(forceRefresh = false) {
|
||||
const songsContainer = document.getElementById('home-recommended-songs');
|
||||
if (songsContainer) {
|
||||
if (forceRefresh) songsContainer.innerHTML = this.createSkeletonTracks(5, true);
|
||||
else if (songsContainer.children.length > 0 && !songsContainer.querySelector('.skeleton')) return; // Already loaded
|
||||
|
||||
try {
|
||||
const seeds = await this.getSeeds();
|
||||
const trackSeeds = seeds.slice(0, 5);
|
||||
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(trackSeeds, 20);
|
||||
|
||||
const filteredTracks = await this.filterUserContent(recommendedTracks, 'track');
|
||||
|
||||
if (filteredTracks.length > 0) {
|
||||
this.renderListWithTracks(songsContainer, filteredTracks, true);
|
||||
} else {
|
||||
songsContainer.innerHTML = createPlaceholder('No song recommendations found.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
artistsContainer.innerHTML = createPlaceholder("You haven't viewed any artists yet.");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
songsContainer.innerHTML = createPlaceholder('Failed to load song recommendations.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playlistsContainer) {
|
||||
const playlists = recents.playlists || [];
|
||||
const mixes = recents.mixes || [];
|
||||
async renderHomeAlbums(forceRefresh = false) {
|
||||
const albumsContainer = document.getElementById('home-recommended-albums');
|
||||
if (albumsContainer) {
|
||||
if (forceRefresh) albumsContainer.innerHTML = this.createSkeletonCards(6);
|
||||
else if (albumsContainer.children.length > 0 && !albumsContainer.querySelector('.skeleton')) return;
|
||||
|
||||
// Note: Since we don't have a unified timestamp for recents in the separate arrays without normalizing,
|
||||
// we will just display playlists then mixes, or interleave them if we wanted to be fancy.
|
||||
// But usually recents are just lists.
|
||||
// Let's just concatenate them.
|
||||
try {
|
||||
const seeds = await this.getSeeds();
|
||||
const albumSeed = seeds.find(t => t.album && t.album.id);
|
||||
if (albumSeed) {
|
||||
const similarAlbums = await this.api.getSimilarAlbums(albumSeed.album.id);
|
||||
const filteredAlbums = await this.filterUserContent(similarAlbums, 'album');
|
||||
|
||||
const combinedRecents = [...playlists, ...mixes]; // Order: Playlists then Mixes
|
||||
|
||||
if (combinedRecents.length) {
|
||||
playlistsContainer.innerHTML = combinedRecents
|
||||
.map((item) => {
|
||||
if (item.isUserPlaylist) {
|
||||
return this.createUserPlaylistCardHTML(item);
|
||||
}
|
||||
if (item.mixType) {
|
||||
// It's a mix
|
||||
return this.createMixCardHTML(item);
|
||||
}
|
||||
return this.createPlaylistCardHTML(item);
|
||||
})
|
||||
.join('');
|
||||
|
||||
combinedRecents.forEach((item) => {
|
||||
if (item.isUserPlaylist) {
|
||||
const el = playlistsContainer.querySelector(`[data-user-playlist-id="${item.id}"]`);
|
||||
if (el) trackDataStore.set(el, item);
|
||||
} else if (item.mixType) {
|
||||
const el = playlistsContainer.querySelector(`[data-mix-id="${item.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, item);
|
||||
this.updateLikeState(el, 'mix', item.id);
|
||||
}
|
||||
if (filteredAlbums.length > 0) {
|
||||
albumsContainer.innerHTML = filteredAlbums.slice(0, 12).map(a => this.createAlbumCardHTML(a)).join('');
|
||||
filteredAlbums.slice(0, 12).forEach(a => {
|
||||
const el = albumsContainer.querySelector(`[data-album-id="${a.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, a);
|
||||
this.updateLikeState(el, 'album', a.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const el = playlistsContainer.querySelector(`[data-playlist-id="${item.uuid}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, item);
|
||||
this.updateLikeState(el, 'playlist', item.uuid);
|
||||
}
|
||||
albumsContainer.innerHTML = `<div style="grid-column: 1/-1; padding: 2rem 0;">${createPlaceholder('Tell us more about what you like so we can recommend albums!')}</div>`;
|
||||
}
|
||||
} else {
|
||||
albumsContainer.innerHTML = `<div style="grid-column: 1/-1; padding: 2rem 0;">${createPlaceholder('Tell us more about what you like so we can recommend albums!')}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
albumsContainer.innerHTML = createPlaceholder('Failed to load album recommendations.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renderHomeArtists(forceRefresh = false) {
|
||||
const artistsContainer = document.getElementById('home-recommended-artists');
|
||||
if (artistsContainer) {
|
||||
if (forceRefresh) artistsContainer.innerHTML = this.createSkeletonCards(6, true);
|
||||
else if (artistsContainer.children.length > 0 && !artistsContainer.querySelector('.skeleton')) return;
|
||||
|
||||
try {
|
||||
const seeds = await this.getSeeds();
|
||||
const artistSeed = seeds.find(t => (t.artist && t.artist.id) || (t.artists && t.artists.length > 0));
|
||||
const artistId = artistSeed ? (artistSeed.artist?.id || artistSeed.artists?.[0]?.id) : null;
|
||||
|
||||
if (artistId) {
|
||||
const similarArtists = await this.api.getSimilarArtists(artistId);
|
||||
const filteredArtists = await this.filterUserContent(similarArtists, 'artist');
|
||||
|
||||
if (filteredArtists.length > 0) {
|
||||
artistsContainer.innerHTML = filteredArtists.slice(0, 12).map(a => this.createArtistCardHTML(a)).join('');
|
||||
filteredArtists.slice(0, 12).forEach(a => {
|
||||
const el = artistsContainer.querySelector(`[data-artist-id="${a.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, a);
|
||||
this.updateLikeState(el, 'artist', a.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
artistsContainer.innerHTML = createPlaceholder('No artist recommendations found.');
|
||||
}
|
||||
} else {
|
||||
artistsContainer.innerHTML = createPlaceholder('Listen to more music to get artist recommendations.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
artistsContainer.innerHTML = createPlaceholder('Failed to load artist recommendations.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderHomeRecent() {
|
||||
const recentContainer = document.getElementById('home-recent-mixed');
|
||||
if (recentContainer) {
|
||||
const recents = recentActivityManager.getRecents();
|
||||
const items = [];
|
||||
|
||||
if (recents.albums) items.push(...recents.albums.slice(0, 4).map(i => ({...i, _kind: 'album'})));
|
||||
if (recents.playlists) items.push(...recents.playlists.slice(0, 4).map(i => ({...i, _kind: 'playlist'})));
|
||||
if (recents.mixes) items.push(...recents.mixes.slice(0, 4).map(i => ({...i, _kind: 'mix'})));
|
||||
|
||||
items.sort(() => Math.random() - 0.5);
|
||||
const displayItems = items.slice(0, 6);
|
||||
|
||||
if (displayItems.length > 0) {
|
||||
recentContainer.innerHTML = displayItems.map(item => {
|
||||
if (item._kind === 'album') return this.createAlbumCardHTML(item);
|
||||
if (item._kind === 'playlist') {
|
||||
if (item.isUserPlaylist) return this.createUserPlaylistCardHTML(item);
|
||||
return this.createPlaylistCardHTML(item);
|
||||
}
|
||||
if (item._kind === 'mix') return this.createMixCardHTML(item);
|
||||
return '';
|
||||
}).join('');
|
||||
|
||||
displayItems.forEach(item => {
|
||||
let selector = '';
|
||||
if (item._kind === 'album') selector = `[data-album-id="${item.id}"]`;
|
||||
else if (item._kind === 'playlist') selector = item.isUserPlaylist ? `[data-user-playlist-id="${item.id}"]` : `[data-playlist-id="${item.uuid}"]`;
|
||||
else if (item._kind === 'mix') selector = `[data-mix-id="${item.id}"]`;
|
||||
|
||||
const el = recentContainer.querySelector(selector);
|
||||
if (el) {
|
||||
trackDataStore.set(el, item);
|
||||
if (item._kind === 'album') this.updateLikeState(el, 'album', item.id);
|
||||
if (item._kind === 'playlist' && !item.isUserPlaylist) this.updateLikeState(el, 'playlist', item.uuid);
|
||||
if (item._kind === 'mix') this.updateLikeState(el, 'mix', item.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
playlistsContainer.innerHTML = createPlaceholder("You haven't viewed any playlists or mixes yet.");
|
||||
recentContainer.innerHTML = createPlaceholder('No recent items yet...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async filterUserContent(items, type) {
|
||||
if (!items || items.length === 0) return [];
|
||||
|
||||
const favorites = await db.getFavorites(type);
|
||||
const favoriteIds = new Set(favorites.map(i => i.id));
|
||||
|
||||
const likedTracks = await db.getFavorites('track');
|
||||
const playlists = await db.getPlaylists(true);
|
||||
|
||||
const userTracksMap = new Map();
|
||||
likedTracks.forEach(t => userTracksMap.set(t.id, t));
|
||||
playlists.forEach(p => {
|
||||
if (p.tracks) p.tracks.forEach(t => userTracksMap.set(t.id, t));
|
||||
});
|
||||
|
||||
if (type === 'track') {
|
||||
return items.filter(item => !userTracksMap.has(item.id));
|
||||
}
|
||||
|
||||
if (type === 'album') {
|
||||
const albumTrackCounts = new Map();
|
||||
for (const track of userTracksMap.values()) {
|
||||
if (track.album && track.album.id) {
|
||||
const aid = track.album.id;
|
||||
albumTrackCounts.set(aid, (albumTrackCounts.get(aid) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return items.filter(item => {
|
||||
if (favoriteIds.has(item.id)) return false;
|
||||
|
||||
const userCount = albumTrackCounts.get(item.id) || 0;
|
||||
const total = item.numberOfTracks;
|
||||
|
||||
if (total && total > 0) {
|
||||
if ((userCount / total) > 0.5) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return items.filter(item => !favoriteIds.has(item.id));
|
||||
}
|
||||
|
||||
async renderSearchPage(query) {
|
||||
this.showPage('search');
|
||||
document.getElementById('search-results-title').textContent = `Search Results for "${query}"`;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,186 +0,0 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Monochrome Legacy - Compatibility</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<link rel="stylesheet" type="text/css" href="legacy.css" />
|
||||
<script type="text/javascript" src="jquery-1.6.4.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
// Simple visual effect or just logging
|
||||
console.log("Compatibility chart loaded.");
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<table
|
||||
width="100%"
|
||||
height="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
>
|
||||
<!-- Header -->
|
||||
<tr height="80">
|
||||
<td colspan="2" bgcolor="#000000" align="center">
|
||||
<font color="#FFFFFF" size="6" face="Comic Sans MS, Arial"
|
||||
><b>Monochrome Music</b></font
|
||||
><br />
|
||||
<font color="#cccccc" size="2">music</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Marquee -->
|
||||
<tr height="20">
|
||||
<td colspan="2" class="marquee-container">
|
||||
<marquee scrollamount="5" scrolldelay="100"
|
||||
>compatibility information</marquee
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Body -->
|
||||
<tr valign="top">
|
||||
<!-- Sidebar -->
|
||||
<td width="150" bgcolor="#dddddd" class="beveled-box">
|
||||
<br />
|
||||
<center><b>Menu</b></center>
|
||||
<hr />
|
||||
<ul style="list-style-type: square; padding-left: 20px">
|
||||
<li class="sidebar-link">
|
||||
<a href="html5.html" id="btn-home">Home</a>
|
||||
</li>
|
||||
<li class="sidebar-link">
|
||||
<a href="compatibility.html">Compatibility</a>
|
||||
</li>
|
||||
<li class="sidebar-link"><a href="/">Modern Site</a></li>
|
||||
</ul>
|
||||
<br />
|
||||
<center>
|
||||
<img
|
||||
src="getie3.gif"
|
||||
alt="IE Logo"
|
||||
width="88"
|
||||
height="31"
|
||||
/><br /><br />
|
||||
<font size="1">free and open software</font><br /><br />
|
||||
<font size="1">made by binimum</font>
|
||||
</center>
|
||||
</td>
|
||||
|
||||
<!-- Content -->
|
||||
<td bgcolor="#ffffff">
|
||||
<div style="padding: 20px">
|
||||
<center><h3>System Requirements</h3></center>
|
||||
<hr />
|
||||
|
||||
<font face="Verdana" size="2">
|
||||
<p>
|
||||
Welcome to the Monochrome Music Legacy Edition connectivity test
|
||||
center.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>Requirement:</b> Any browser that supports
|
||||
<a href="https://caniuse.com/aac">AAC Audio</a> or
|
||||
<a href="https://caniuse.com/flac">FLAC Audio</a>.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="beveled-box"
|
||||
style="padding: 10px; background-color: #eeeeee"
|
||||
>
|
||||
<h4>Officially supported: (HTML5)</h4>
|
||||
<ul>
|
||||
<li><b>Internet Explorer</b> 9+</li>
|
||||
<li><b>Google Chrome</b> 12+</li>
|
||||
<li><b>Safari</b> 5+</li>
|
||||
<li>
|
||||
<b>Firefox</b> 22+ (must have AAC codecs installed - so not
|
||||
WinXP/Win7 by default until Firefox 51+!)
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<i
|
||||
>Note: Older browsers (IE 5.5-8) may work if you have Adobe
|
||||
Flash Player installed.</i
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<i
|
||||
>Note 2: On many older browsers you will need to visit the
|
||||
site <b>using HTTP and NOT HTTPS</b> due to outdated SSL.</i
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<table width="100%" border="1" cellpadding="5" cellspacing="0">
|
||||
<tr bgcolor="#cccccc">
|
||||
<th>Browser</th>
|
||||
<th>Min version</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Internet Explorer</td>
|
||||
<td>9</td>
|
||||
<td>
|
||||
<font color="green"><b>Tested (AAC)</b></font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Google Chrome</td>
|
||||
<td>15</td>
|
||||
<td>
|
||||
<font color="green"><b>Tested (AAC)</b></font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>56</td>
|
||||
<td>
|
||||
<font color="green"><b>Tested (FLAC)</b></font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Safari</td>
|
||||
<td>5</td>
|
||||
<td>
|
||||
<font color="green"><b>Tested on 5.1 (AAC)</b></font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td>
|
||||
<font color="green"><b>Tested on 13.1 (FLAC)</b></font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Firefox</td>
|
||||
<td>22</td>
|
||||
<td>
|
||||
<font color="orange"
|
||||
><b>Partial (AAC requires codecs)</b></font
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>52</td>
|
||||
<td>
|
||||
<font color="green"><b>Tested (FLAC)</b></font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="html5.html"><b><< Return to Player</b></a>
|
||||
</p>
|
||||
</font>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
|
|
@ -1,113 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Monochrome Legacy (HTML5)</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<link rel="stylesheet" type="text/css" href="legacy.css" />
|
||||
<script type="text/javascript" src="jquery-1.6.4.min.js"></script>
|
||||
<script type="text/javascript" src="html5.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<table
|
||||
width="100%"
|
||||
height="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
>
|
||||
<!-- Header -->
|
||||
<tr height="80">
|
||||
<td colspan="2" bgcolor="#000000" align="center">
|
||||
<font color="#FFFFFF" size="6" face="Comic Sans MS, Arial"
|
||||
><b>Monochrome Music</b></font
|
||||
><br />
|
||||
<font color="#cccccc" size="2">music</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Marquee -->
|
||||
<tr height="20">
|
||||
<td colspan="2" class="marquee-container">
|
||||
<marquee scrollamount="5" scrolldelay="100"
|
||||
>Welcome to Monochrome Music Legacy Edition check out the latest
|
||||
hits new songs added daily under construction sign
|
||||
guestbook</marquee
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Body -->
|
||||
<tr valign="top">
|
||||
<!-- Sidebar -->
|
||||
<td width="150" bgcolor="#dddddd" class="beveled-box">
|
||||
<br />
|
||||
<center><b>Menu</b></center>
|
||||
<hr />
|
||||
<ul style="list-style-type: square; padding-left: 20px">
|
||||
<li class="sidebar-link"><a href="#" id="btn-home">Home</a></li>
|
||||
<li class="sidebar-link">
|
||||
<a href="compatibility.html">Compatibility</a>
|
||||
</li>
|
||||
<!-- Search is now inline below -->
|
||||
<li class="sidebar-link"><a href="/">Go to Modern Site</a></li>
|
||||
</ul>
|
||||
<br />
|
||||
<!-- Search Form moved to main content -->
|
||||
<br />
|
||||
<center>
|
||||
<img
|
||||
src="getie3.gif"
|
||||
alt="IE Logo"
|
||||
width="88"
|
||||
height="31"
|
||||
/><br /><br />
|
||||
<font size="1">Free and open software</font><br /><br />
|
||||
<font size="1">made by binimum</font>
|
||||
</center>
|
||||
</td>
|
||||
|
||||
<!-- Content -->
|
||||
<td bgcolor="#ffffff">
|
||||
<div style="padding: 10px">
|
||||
<!-- Now Playing / Player Control -->
|
||||
<div class="controls" style="margin-bottom: 20px">
|
||||
<font face="Verdana" size="2">
|
||||
<span id="now-playing-info">Status: Ready to play.</span>
|
||||
</font>
|
||||
<br /><br />
|
||||
<audio
|
||||
id="audio-player"
|
||||
controls
|
||||
preload="none"
|
||||
style="width: 300px; height: 45px"
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<!-- Moved Search Form -->
|
||||
<div
|
||||
class="beveled-box"
|
||||
style="margin-bottom: 20px; text-align: center"
|
||||
>
|
||||
<b>Search Music: </b>
|
||||
<form
|
||||
id="search-form"
|
||||
onsubmit="return false;"
|
||||
style="display: inline"
|
||||
>
|
||||
<input type="text" id="search-input" size="40" />
|
||||
<input type="submit" id="btn-search" value="Search" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="main-content">
|
||||
<center><h3>Loading...</h3></center>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
571
legacy/html5.js
571
legacy/html5.js
|
|
@ -1,571 +0,0 @@
|
|||
// jQuery 1.12.4 Legacy Support
|
||||
$(document).ready(function () {
|
||||
// CORS Support for IE8/9 (XDomainRequest)
|
||||
// Required because IE8/9 do not support CORS via standard XMLHttpRequest
|
||||
// Note: $.ajaxTransport was added in jQuery 1.5. If using 1.2.6, this will be skipped.
|
||||
if (window.XDomainRequest && $.ajaxTransport) {
|
||||
$.ajaxTransport(function(s) {
|
||||
if (s.crossDomain && s.async) {
|
||||
if (s.timeout) {
|
||||
s.xdrTimeout = s.timeout;
|
||||
delete s.timeout;
|
||||
}
|
||||
var xdr;
|
||||
return {
|
||||
send: function(_, complete) {
|
||||
function callback(status, statusText, responses, responseHeaders) {
|
||||
xdr.onload = xdr.onerror = xdr.ontimeout = $.noop;
|
||||
xdr = undefined;
|
||||
complete(status, statusText, responses, responseHeaders);
|
||||
}
|
||||
xdr = new XDomainRequest();
|
||||
try {
|
||||
xdr.open(s.type, s.url);
|
||||
xdr.onload = function() {
|
||||
callback(200, "OK", { text: xdr.responseText }, "Content-Type: " + xdr.contentType);
|
||||
};
|
||||
xdr.onerror = function() {
|
||||
callback(404, "Not Found");
|
||||
};
|
||||
xdr.ontimeout = function() {
|
||||
callback(0, "timeout");
|
||||
};
|
||||
xdr.timeout = s.xdrTimeout || Number.MAX_VALUE;
|
||||
xdr.send((s.hasContent && s.data) || null);
|
||||
} catch(e) {
|
||||
// Protocol Mismatch generally throws here in IE
|
||||
callback(500, "Protocol/Access Error", { text: e.message });
|
||||
}
|
||||
},
|
||||
abort: function() {
|
||||
if (xdr) {
|
||||
xdr.onerror = $.noop;
|
||||
xdr.abort();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
// REMOVED: $.support.cors = true;
|
||||
// We MUST NOT set this for IE9. If we do, jQuery tries to use standard XHR for cross-domain,
|
||||
// which fails ("Access is denied"). Leaving it false forces jQuery to use our custom XDR transport.
|
||||
|
||||
var apiInstances = [];
|
||||
var currentInstanceIndex = 0;
|
||||
var isHttpFallback = false;
|
||||
var FALLBACK_INSTANCES = [
|
||||
"https://wolf.qqdl.site",
|
||||
"https://maus.qqdl.site",
|
||||
"https://vogel.qqdl.site",
|
||||
"https://katze.qqdl.site",
|
||||
"https://hund.qqdl.site",
|
||||
"https://tidal.kinoplus.online",
|
||||
"https://tidal-api.binimum.org",
|
||||
];
|
||||
var audioPlayer = $("#audio-player")[0];
|
||||
var currentTrackInfo = $("#now-playing-info");
|
||||
|
||||
// HTML5 Version - No Flash/SoundJS (Cleared)
|
||||
|
||||
// Initial Load
|
||||
// Run HTTPS probe first
|
||||
checkHttpsSupport(function() {
|
||||
fetchInstances(function () {
|
||||
loadRecentTracks();
|
||||
});
|
||||
});
|
||||
|
||||
function checkHttpsSupport(callback) {
|
||||
if (window.location.protocol === "http:") {
|
||||
// If we are already on HTTP, we might want to check if HTTPS is possible?
|
||||
// Or just assume if user loaded via HTTP, we might need HTTP for APIs too.
|
||||
// But user might be on HTTP because they typed it, but API supports HTTPS.
|
||||
// Let's Probe.
|
||||
// However, if we are on HTTPS, mixed content blocking might prevent HTTP fallback checking?
|
||||
// Actually, we want to know if Client supports HTTPS.
|
||||
}
|
||||
|
||||
// Probe a known HTTPS endpoint (one of our instances)
|
||||
// Use a known stable one, or just try the first instance later?
|
||||
// Better to fail fast now.
|
||||
var probeUrl = "https://tidal.kinoplus.online/";
|
||||
|
||||
console.log("Probing HTTPS support...");
|
||||
|
||||
var probeSuccess = false;
|
||||
var probeFinished = false;
|
||||
|
||||
function finishProbe(success) {
|
||||
if (probeFinished) return;
|
||||
probeFinished = true;
|
||||
if (success) {
|
||||
console.log("HTTPS Probe Successful.");
|
||||
isHttpFallback = false;
|
||||
} else {
|
||||
console.log("HTTPS Probe Failed. Defaulting to HTTP fallback.");
|
||||
isHttpFallback = true;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
var timeout = setTimeout(function() {
|
||||
finishProbe(false);
|
||||
}, 5000); // 3s timeout for HTTPS check
|
||||
|
||||
try {
|
||||
$.ajax({
|
||||
url: probeUrl,
|
||||
dataType: "json",
|
||||
timeout: 2500, // jQuery timeout
|
||||
success: function() {
|
||||
finishProbe(true);
|
||||
},
|
||||
error: function() {
|
||||
finishProbe(false); // XHR Error or Timeout
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
finishProbe(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Event Bindings
|
||||
$("#btn-home").click(function (e) {
|
||||
e.preventDefault();
|
||||
loadRecentTracks();
|
||||
});
|
||||
|
||||
$("#search-form").submit(function (e) {
|
||||
e.preventDefault();
|
||||
var query = $("#search-input").val();
|
||||
performSearch(query);
|
||||
});
|
||||
|
||||
// Global functions exposed for inline onclicks
|
||||
// Global functions exposed for inline onclicks
|
||||
// Global Stop function to prevent overlap
|
||||
function stopAllAudio() {
|
||||
// 1. Stop SoundJS (Removed in HTML5 ver)
|
||||
|
||||
// 2. Stop DOM Player
|
||||
if (audioPlayer) {
|
||||
try {
|
||||
audioPlayer.pause();
|
||||
audioPlayer.currentTime = 0;
|
||||
// Don't clear src immediately as it might flash, just pause.
|
||||
} catch(e) { }
|
||||
}
|
||||
}
|
||||
|
||||
window.playTrack = function (id, attemptFallback) {
|
||||
var quality = attemptFallback ? "HIGH" : "LOSSLESS";
|
||||
apiRequest(
|
||||
"/track/?id=" + id + "&quality=" + quality,
|
||||
function (data) {
|
||||
if (data && data.data && data.data.manifest) {
|
||||
try {
|
||||
var manifestStr = base64Decode(data.data.manifest);
|
||||
var manifest = JSON.parse(manifestStr);
|
||||
if (manifest.urls && manifest.urls.length > 0) {
|
||||
var streamUrl = manifest.urls[0];
|
||||
|
||||
// Unified Playback Strategy:
|
||||
// 1. Stop Everything
|
||||
stopAllAudio();
|
||||
|
||||
// 2. Try Native DOM Player (Visible Interface)
|
||||
// If this works, user gets controls. If it fails (IE), we fallback to SoundJS.
|
||||
playNativeFirst(streamUrl, id, quality);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Manifest error: " + e);
|
||||
}
|
||||
} else {
|
||||
handleError("Invalid track data");
|
||||
}
|
||||
},
|
||||
function (err) {
|
||||
handleError(err);
|
||||
}
|
||||
);
|
||||
|
||||
function playNativeFirst(url, id, quality, isRetry) {
|
||||
var domPlayer = $("#audio-player")[0];
|
||||
var playbackTimer = null;
|
||||
|
||||
// Quality Label
|
||||
var qLabel = (quality === "LOSSLESS") ? " (FLAC)" : " (AAC)";
|
||||
if (attemptFallback) qLabel = " (AAC)";
|
||||
|
||||
// Basic check for audio support
|
||||
if (domPlayer && typeof domPlayer.play === 'function') {
|
||||
|
||||
// Explicitly check for Codec support
|
||||
if (quality === "LOSSLESS") {
|
||||
// FLAC check
|
||||
var canPlay = "";
|
||||
try {
|
||||
canPlay = domPlayer.canPlayType("audio/flac");
|
||||
} catch(e) {}
|
||||
|
||||
if (canPlay === "" || canPlay === "no") {
|
||||
console.log("Browser reports no FLAC support. Fallback to AAC.");
|
||||
if (!attemptFallback) {
|
||||
window.playTrack(id, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus("Starting Native Playback" + qLabel + ((isRetry) ? " (HTTP)..." : "..."));
|
||||
|
||||
// Helper to handle retry vs legacy fallback
|
||||
function triggerRetryOrLegacy(msg) {
|
||||
if (playbackTimer) {
|
||||
clearTimeout(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
|
||||
// Try HTTP fallback if SSL failed
|
||||
if (!isRetry && url.indexOf("https://") === 0) {
|
||||
console.log("HTTPS failed/timeout (" + msg + "), retrying with HTTP...");
|
||||
var httpUrl = "http://" + url.substring(8);
|
||||
playNativeFirst(httpUrl, id, quality, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If native fails on HTML5 site, we have no Flash fallback.
|
||||
handleError("Playback Failed - No Flash Fallback Available (" + msg + ")");
|
||||
}
|
||||
|
||||
// Set error handler for THIS attempt
|
||||
domPlayer.onerror = function() {
|
||||
var errCode = domPlayer.error ? domPlayer.error.code : 0;
|
||||
console.log("Native Error Code: " + errCode);
|
||||
triggerRetryOrLegacy("onerror: " + errCode);
|
||||
};
|
||||
|
||||
try {
|
||||
domPlayer.src = url;
|
||||
domPlayer.preload = "auto";
|
||||
domPlayer.load(); // Force reload/buffering
|
||||
|
||||
var playPromise = domPlayer.play();
|
||||
|
||||
// Set a safety timeout for "forever pending" requests (common in Chrome 15 with SSL issues)
|
||||
playbackTimer = setTimeout(function() {
|
||||
console.log("Playback timeout - stalling detected.");
|
||||
triggerRetryOrLegacy("timeout");
|
||||
}, 5000); // 5 seconds to start playing
|
||||
|
||||
// If playback starts, clear timeout
|
||||
domPlayer.onplaying = function() {
|
||||
if (playbackTimer) {
|
||||
clearTimeout(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
updateStatus("Now Playing..." + qLabel);
|
||||
};
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(function() {
|
||||
// Promise resolved doesn't always mean playing started (buffering)
|
||||
// But usually it means intent is accepted.
|
||||
// We keep timer running until 'onplaying' checks in?
|
||||
// Actually promise resolve just means "accepted".
|
||||
// Chrome 15 won't have promise.
|
||||
// Modern browsers: resolve -> wait for data -> playing.
|
||||
// If data hangs, promise resolved but playing never fires.
|
||||
// So we keep timer.
|
||||
})
|
||||
.catch(function(e) {
|
||||
console.log("Native Play Promise Rejected: " + e.name);
|
||||
triggerRetryOrLegacy("promise rejection: " + e.name);
|
||||
});
|
||||
} else {
|
||||
// Legacy browser (no promise)
|
||||
// Wait for onplaying or timeout
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Native Exception: " + e.message);
|
||||
triggerRetryOrLegacy("exception: " + e.message);
|
||||
}
|
||||
|
||||
} else {
|
||||
// No native audio support (IE < 9)
|
||||
handleError("No Native Audio Support");
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(msg) {
|
||||
if (currentTrackInfo.length) {
|
||||
currentTrackInfo.html(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(msg) {
|
||||
if (!attemptFallback) {
|
||||
window.playTrack(id, true);
|
||||
} else {
|
||||
// alert("Playback Error: " + (msg || "Unknown"));
|
||||
updateStatus("Error: " + msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function loadRecentTracks() {
|
||||
setContent("Loading recent tracks...");
|
||||
apiRequest(
|
||||
"/search/?s=a&limit=20",
|
||||
function (data) {
|
||||
if (data && data.data && data.data.items) {
|
||||
renderTracks(data.data.items, "Recently Added / Popular");
|
||||
} else {
|
||||
setContent("No recent tracks found.");
|
||||
}
|
||||
},
|
||||
function (err) {
|
||||
setContent("Error loading tracks: " + err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function fetchInstances(callback) {
|
||||
// using $.ajax directly to handle errors robustly
|
||||
$.ajax({
|
||||
url: "/instances.json",
|
||||
dataType: "json",
|
||||
success: function (instances) {
|
||||
if (instances && instances.length > 0) {
|
||||
apiInstances = shuffleArray(instances);
|
||||
// Clean URLs
|
||||
for (var i = 0; i < apiInstances.length; i++) {
|
||||
if (apiInstances[i].charAt(apiInstances[i].length - 1) === "/") {
|
||||
apiInstances[i] = apiInstances[i].substring(0, apiInstances[i].length - 1);
|
||||
}
|
||||
}
|
||||
currentInstanceIndex = 0;
|
||||
callback();
|
||||
} else {
|
||||
useFallback(callback);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
useFallback(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useFallback(callback) {
|
||||
apiInstances = shuffleArray(FALLBACK_INSTANCES.slice()); // Copy and shuffle
|
||||
currentInstanceIndex = 0;
|
||||
callback();
|
||||
}
|
||||
|
||||
function shuffleArray(array) {
|
||||
for (var i = array.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function performSearch(query) {
|
||||
var resultsDiv = $("#search-results");
|
||||
if (resultsDiv.length === 0) {
|
||||
setContent('<div id="search-results">Searching...</div>');
|
||||
resultsDiv = $("#search-results");
|
||||
} else {
|
||||
resultsDiv.html("Searching...");
|
||||
}
|
||||
|
||||
apiRequest(
|
||||
"/search/?s=" + encodeURIComponent(query) + "&limit=25",
|
||||
function (data) {
|
||||
var tracks = (data && data.data && data.data.items) ? data.data.items : [];
|
||||
if (tracks.length === 0) {
|
||||
resultsDiv.html("No results found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var html =
|
||||
'<table width="100%" border="1" cellpadding="2" cellspacing="0">';
|
||||
html +=
|
||||
'<tr bgcolor="#bbbbbb"><th>Play</th><th>Title</th><th>Artist</th><th>Album</th></tr>';
|
||||
|
||||
$.each(tracks, function (i, t) {
|
||||
var safeTitle = escapeHtml(t.title);
|
||||
var safeArtist = escapeHtml(t.artist.name);
|
||||
var safeAlbum = escapeHtml(t.album.title);
|
||||
|
||||
html += '<tr class="track-row">';
|
||||
html +=
|
||||
'<td align="center"><button onclick="window.playTrack(\'' +
|
||||
t.id +
|
||||
"')\">Play</button></td>";
|
||||
html += "<td>" + safeTitle + "</td>";
|
||||
html += "<td>" + safeArtist + "</td>";
|
||||
html += "<td>" + safeAlbum + "</td>";
|
||||
html += "</tr>";
|
||||
});
|
||||
html += "</table>";
|
||||
|
||||
resultsDiv.html(html);
|
||||
},
|
||||
function (err) {
|
||||
resultsDiv.html("Error: " + err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function renderTracks(tracks, title) {
|
||||
var html = "<h3>" + title + "</h3>";
|
||||
html += '<table width="100%" border="1" cellpadding="2" cellspacing="0">';
|
||||
html +=
|
||||
'<tr bgcolor="#bbbbbb"><th>Play</th><th>Title</th><th>Artist</th><th>Album</th></tr>';
|
||||
|
||||
$.each(tracks, function (i, t) {
|
||||
var safeTitle = escapeHtml(t.title);
|
||||
var safeArtist = escapeHtml(t.artist.name);
|
||||
var safeAlbum = escapeHtml(t.album.title);
|
||||
|
||||
html += '<tr class="track-row">';
|
||||
html +=
|
||||
'<td align="center"><button onclick="window.playTrack(\'' +
|
||||
t.id +
|
||||
"')\">Play</button></td>";
|
||||
html += "<td>" + safeTitle + "</td>";
|
||||
html += "<td>" + safeArtist + "</td>";
|
||||
html += "<td>" + safeAlbum + "</td>";
|
||||
html += "</tr>";
|
||||
});
|
||||
html += "</table>";
|
||||
|
||||
setContent(html);
|
||||
}
|
||||
|
||||
function setContent(html) {
|
||||
$("#main-content").html(html);
|
||||
}
|
||||
|
||||
function apiRequest(endpoint, success, error) {
|
||||
if (apiInstances.length === 0) {
|
||||
error("No API instances available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentBaseUrl = apiInstances[currentInstanceIndex];
|
||||
var finalUrl = currentBaseUrl;
|
||||
|
||||
// Check for HTTP fallback
|
||||
if (isHttpFallback) {
|
||||
// If original was https, downgrade it
|
||||
if (finalUrl.indexOf("https://") === 0) {
|
||||
finalUrl = "http://" + finalUrl.substring(8);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$.ajax({
|
||||
url: finalUrl + endpoint,
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
// Logic: If successful with HTTP fallback, maybe we should stick to it?
|
||||
// For now, we just proceed.
|
||||
success(data);
|
||||
},
|
||||
error: function (xhr, status, errorThrown) {
|
||||
handleApiError(endpoint, success, error, status + " (" + errorThrown + ")");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
handleApiError(endpoint, success, error, "Exception: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function handleApiError(endpoint, success, error, errorMsg) {
|
||||
// 1. Try HTTP fallback for current instance if allowed
|
||||
if (!isHttpFallback && window.location.protocol !== "https:") {
|
||||
// Only if current instance is HTTPS
|
||||
if (apiInstances[currentInstanceIndex].indexOf("https://") === 0) {
|
||||
isHttpFallback = true;
|
||||
apiRequest(endpoint, success, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Move to next instance
|
||||
isHttpFallback = false; // Reset for next instance
|
||||
currentInstanceIndex++;
|
||||
|
||||
if (currentInstanceIndex < apiInstances.length) {
|
||||
// Retry with next instance
|
||||
apiRequest(endpoint, success, error);
|
||||
} else {
|
||||
// All instances failed
|
||||
// We could try to reset index and wait, but for now we fail.
|
||||
// Or maybe we should loop back to 0? But infinite loops are bad.
|
||||
// Let's just fail after one full rotation.
|
||||
currentInstanceIndex = 0; // Reset for next user interaction attempt
|
||||
error("All API instances failed. Last error: " + errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
function playWithEmbed(url) {
|
||||
var container = $("#audio-container");
|
||||
if (container.length === 0) {
|
||||
if (audioPlayer && audioPlayer.parentNode) {
|
||||
$(audioPlayer.parentNode).attr("id", "audio-container");
|
||||
container = $("#audio-container");
|
||||
}
|
||||
}
|
||||
|
||||
if (container.length) {
|
||||
var embedDiv = $("#embed-container");
|
||||
if (embedDiv.length === 0) {
|
||||
embedDiv = $('<div id="embed-container"></div>');
|
||||
container.append(embedDiv);
|
||||
}
|
||||
// Use html() to set innerHTML properly
|
||||
var embedHtml = '<embed type="application/x-mplayer2" src="' + url + '" autostart="true" width="0" height="0" enablejavascript="true"></embed>';
|
||||
embedDiv.html(embedHtml);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function base64Decode(str) {
|
||||
if (window.atob) {
|
||||
return window.atob(str);
|
||||
}
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
var output = "";
|
||||
str = String(str).replace(/=+$/, '');
|
||||
if (str.length % 4 == 1) {
|
||||
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
|
||||
}
|
||||
for (
|
||||
var bc = 0, bs = 0, buffer, i = 0;
|
||||
buffer = str.charAt(i++);
|
||||
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
|
||||
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,133 +1,16 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Monochrome Legacy</title>
|
||||
|
||||
<script>
|
||||
// HTML5 Redirection Logic
|
||||
// If browser supports Canvas and Audio, redirect to the HTML5 version.
|
||||
// This targets Chrome 10+, Safari 3+, IE9+, Firefox 3.5+
|
||||
(function () {
|
||||
var hasCanvas = !!document.createElement("canvas").getContext;
|
||||
var hasAudio = !!(window.Audio && new window.Audio().canPlayType);
|
||||
|
||||
// Prevent redirect loop if already there (check filename not needed if files differ, but safe)
|
||||
if (hasCanvas && hasAudio) {
|
||||
// Check if user manually opted out? (Optional, skipping for now)
|
||||
if (window.location.href.indexOf("html5.html") === -1) {
|
||||
window.location.href = "html5.html";
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="legacy.css" />
|
||||
<script type="text/javascript" src="jquery.min.js"></script>
|
||||
<script type="text/javascript" src="swfobject.js"></script>
|
||||
<script type="text/javascript" src="soundjs-0.5.2.min.js"></script>
|
||||
<script type="text/javascript" src="flashplugin.js"></script>
|
||||
<script type="text/javascript" src="legacy.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<table
|
||||
width="100%"
|
||||
height="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
>
|
||||
<!-- Header -->
|
||||
<tr height="80">
|
||||
<td colspan="2" bgcolor="#000000" align="center">
|
||||
<font color="#FFFFFF" size="6" face="Comic Sans MS, Arial"
|
||||
><b>Monochrome Music</b></font
|
||||
><br />
|
||||
<font color="#cccccc" size="2">music</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Marquee -->
|
||||
<tr height="20">
|
||||
<td colspan="2" class="marquee-container">
|
||||
<marquee scrollamount="5" scrolldelay="100"
|
||||
>Welcome to Monochrome Music Legacy Edition check out the latest
|
||||
hits new songs added daily under construction sign
|
||||
guestbook</marquee
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Body -->
|
||||
<tr valign="top">
|
||||
<!-- Sidebar -->
|
||||
<td width="150" bgcolor="#dddddd" class="beveled-box">
|
||||
<br />
|
||||
<center><b>Menu</b></center>
|
||||
<hr />
|
||||
<ul style="list-style-type: square; padding-left: 20px">
|
||||
<li class="sidebar-link"><a href="#" id="btn-home">Home</a></li>
|
||||
<li class="sidebar-link">
|
||||
<a href="compatibility.html">Compatibility</a>
|
||||
</li>
|
||||
<!-- Search is now inline below -->
|
||||
<li class="sidebar-link"><a href="/">Go to Modern Site</a></li>
|
||||
</ul>
|
||||
<br />
|
||||
<!-- Search Form moved to main content -->
|
||||
<br />
|
||||
<center>
|
||||
<img
|
||||
src="getie3.gif"
|
||||
alt="IE Logo"
|
||||
width="88"
|
||||
height="31"
|
||||
/><br /><br />
|
||||
<font size="1">Free and open software</font><br /><br />
|
||||
<font size="1">made by binimum</font><br /><br />
|
||||
</center>
|
||||
</td>
|
||||
|
||||
<!-- Content -->
|
||||
<td bgcolor="#ffffff">
|
||||
<div style="padding: 10px">
|
||||
<!-- Now Playing / Player Control -->
|
||||
<div class="controls" style="margin-bottom: 20px">
|
||||
<font face="Verdana" size="2">
|
||||
<span id="now-playing-info">Status: Ready to play.</span>
|
||||
</font>
|
||||
<br /><br />
|
||||
<audio
|
||||
id="audio-player"
|
||||
controls
|
||||
preload="none"
|
||||
style="width: 300px; height: 45px"
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<!-- Moved Search Form -->
|
||||
<div
|
||||
class="beveled-box"
|
||||
style="margin-bottom: 20px; text-align: center"
|
||||
>
|
||||
<b>Search Music: </b>
|
||||
<form
|
||||
id="search-form"
|
||||
onsubmit="return false;"
|
||||
style="display: inline"
|
||||
>
|
||||
<input type="text" id="search-input" size="40" />
|
||||
<input type="submit" id="btn-search" value="Search" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="main-content">
|
||||
<center><h3>Loading...</h3></center>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; URL='https://monochrome.binimum.org/legacy'" />
|
||||
<script>
|
||||
window.location.href = 'https://monochrome.binimum.org/legacy';
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>If you are not redirected, <a href="https://monochrome.binimum.org/legacy">click here</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
4
legacy/jquery-1.6.4.min.js
vendored
4
legacy/jquery-1.6.4.min.js
vendored
File diff suppressed because one or more lines are too long
32
legacy/jquery.min.js
vendored
32
legacy/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,97 +0,0 @@
|
|||
/* Retro 90s Styling */
|
||||
body {
|
||||
background-color: #c0c0c0;
|
||||
font-family: "Verdana", "Arial", "Helvetica", sans-serif, "Times New Roman", Times, serif;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000000;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse; /* Not strictly 90s but makes it cleaner */
|
||||
}
|
||||
|
||||
.beveled-box {
|
||||
border-top: 2px solid #ffffff;
|
||||
border-left: 2px solid #ffffff;
|
||||
border-right: 2px solid #808080;
|
||||
border-bottom: 2px solid #808080;
|
||||
background-color: #c0c0c0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.inset-box {
|
||||
border-top: 2px solid #808080;
|
||||
border-left: 2px solid #808080;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
background-color: #ffffff;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.marquee-container {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.track-row td {
|
||||
padding: 2px 5px;
|
||||
border: 1px solid #808080;
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background-color: #a0a0a0;
|
||||
border: 2px outset #ffffff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #c0c0c0;
|
||||
border-top: 2px solid #ffffff;
|
||||
border-left: 2px solid #ffffff;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
padding: 2px 10px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:active {
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
}
|
||||
635
legacy/legacy.js
635
legacy/legacy.js
|
|
@ -1,635 +0,0 @@
|
|||
// jQuery 1.12.4 Legacy Support
|
||||
$(document).ready(function () {
|
||||
// CORS Support for IE8/9 (XDomainRequest)
|
||||
// Required because IE8/9 do not support CORS via standard XMLHttpRequest
|
||||
if (window.XDomainRequest) {
|
||||
$.ajaxTransport(function(s) {
|
||||
if (s.crossDomain && s.async) {
|
||||
if (s.timeout) {
|
||||
s.xdrTimeout = s.timeout;
|
||||
delete s.timeout;
|
||||
}
|
||||
var xdr;
|
||||
return {
|
||||
send: function(_, complete) {
|
||||
function callback(status, statusText, responses, responseHeaders) {
|
||||
xdr.onload = xdr.onerror = xdr.ontimeout = $.noop;
|
||||
xdr = undefined;
|
||||
complete(status, statusText, responses, responseHeaders);
|
||||
}
|
||||
xdr = new XDomainRequest();
|
||||
try {
|
||||
xdr.open(s.type, s.url);
|
||||
xdr.onload = function() {
|
||||
callback(200, "OK", { text: xdr.responseText }, "Content-Type: " + xdr.contentType);
|
||||
};
|
||||
xdr.onerror = function() {
|
||||
callback(404, "Not Found");
|
||||
};
|
||||
xdr.ontimeout = function() {
|
||||
callback(0, "timeout");
|
||||
};
|
||||
xdr.timeout = s.xdrTimeout || Number.MAX_VALUE;
|
||||
xdr.send((s.hasContent && s.data) || null);
|
||||
} catch(e) {
|
||||
// Protocol Mismatch generally throws here in IE
|
||||
callback(500, "Protocol/Access Error", { text: e.message });
|
||||
}
|
||||
},
|
||||
abort: function() {
|
||||
if (xdr) {
|
||||
xdr.onerror = $.noop;
|
||||
xdr.abort();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
// REMOVED: $.support.cors = true;
|
||||
// We MUST NOT set this for IE9. If we do, jQuery tries to use standard XHR for cross-domain,
|
||||
// which fails ("Access is denied"). Leaving it false forces jQuery to use our custom XDR transport.
|
||||
|
||||
var apiInstances = [];
|
||||
var currentInstanceIndex = 0;
|
||||
var isHttpFallback = false;
|
||||
var FALLBACK_INSTANCES = [
|
||||
"https://wolf.qqdl.site",
|
||||
"https://maus.qqdl.site",
|
||||
"https://vogel.qqdl.site",
|
||||
"https://katze.qqdl.site",
|
||||
"https://hund.qqdl.site",
|
||||
"https://tidal.kinoplus.online",
|
||||
"https://tidal-api.binimum.org",
|
||||
];
|
||||
var audioPlayer = $("#audio-player")[0];
|
||||
var currentTrackInfo = $("#now-playing-info");
|
||||
|
||||
// Initialize SoundJS
|
||||
// Note: Class is FlashPlugin in 0.5.2, but SWF is FlashAudioPlugin.swf
|
||||
createjs.FlashPlugin.swfPath = "./";
|
||||
// Custom Architecture:
|
||||
// We handle HTML5 Audio manually via playNativeFirst() for full control (UI, timeouts, hacks).
|
||||
// SoundJS is reserved STRICTLY for Flash fallback on legacy browsers (IE, old Chrome).
|
||||
// Therefore, we ONLY register the FlashPlugin.
|
||||
createjs.Sound.registerPlugins([createjs.FlashPlugin]);
|
||||
|
||||
// Initial Load
|
||||
// Run HTTPS probe first
|
||||
checkHttpsSupport(function() {
|
||||
fetchInstances(function () {
|
||||
loadRecentTracks();
|
||||
});
|
||||
});
|
||||
|
||||
function checkHttpsSupport(callback) {
|
||||
if (window.location.protocol === "http:") {
|
||||
// If we are already on HTTP, we might want to check if HTTPS is possible?
|
||||
// Or just assume if user loaded via HTTP, we might need HTTP for APIs too.
|
||||
// But user might be on HTTP because they typed it, but API supports HTTPS.
|
||||
// Let's Probe.
|
||||
// However, if we are on HTTPS, mixed content blocking might prevent HTTP fallback checking?
|
||||
// Actually, we want to know if Client supports HTTPS.
|
||||
}
|
||||
|
||||
// Probe a known HTTPS endpoint (one of our instances)
|
||||
// Use a known stable one, or just try the first instance later?
|
||||
// Better to fail fast now.
|
||||
var probeUrl = "https://tidal.kinoplus.online/";
|
||||
|
||||
console.log("Probing HTTPS support...");
|
||||
|
||||
var probeSuccess = false;
|
||||
var probeFinished = false;
|
||||
|
||||
function finishProbe(success) {
|
||||
if (probeFinished) return;
|
||||
probeFinished = true;
|
||||
if (success) {
|
||||
console.log("HTTPS Probe Successful.");
|
||||
isHttpFallback = false;
|
||||
} else {
|
||||
console.log("HTTPS Probe Failed. Defaulting to HTTP fallback.");
|
||||
isHttpFallback = true;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
var timeout = setTimeout(function() {
|
||||
finishProbe(false);
|
||||
}, 5000); // 3s timeout for HTTPS check
|
||||
|
||||
try {
|
||||
$.ajax({
|
||||
url: probeUrl,
|
||||
dataType: "json",
|
||||
timeout: 2500, // jQuery timeout
|
||||
success: function() {
|
||||
finishProbe(true);
|
||||
},
|
||||
error: function() {
|
||||
finishProbe(false); // XHR Error or Timeout
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
finishProbe(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Event Bindings
|
||||
$("#btn-home").click(function (e) {
|
||||
e.preventDefault();
|
||||
loadRecentTracks();
|
||||
});
|
||||
|
||||
$("#search-form").submit(function (e) {
|
||||
e.preventDefault();
|
||||
var query = $("#search-input").val();
|
||||
performSearch(query);
|
||||
});
|
||||
|
||||
// Global functions exposed for inline onclicks
|
||||
// Global functions exposed for inline onclicks
|
||||
// Global Stop function to prevent overlap
|
||||
function stopAllAudio() {
|
||||
// 1. Stop SoundJS
|
||||
if (typeof createjs !== "undefined" && createjs.Sound) {
|
||||
createjs.Sound.stop();
|
||||
}
|
||||
|
||||
// 2. Stop DOM Player
|
||||
if (audioPlayer) {
|
||||
try {
|
||||
audioPlayer.pause();
|
||||
audioPlayer.currentTime = 0;
|
||||
// Don't clear src immediately as it might flash, just pause.
|
||||
} catch(e) { }
|
||||
}
|
||||
}
|
||||
|
||||
window.playTrack = function (id, attemptFallback) {
|
||||
var quality = attemptFallback ? "HIGH" : "LOSSLESS";
|
||||
apiRequest(
|
||||
"/track/?id=" + id + "&quality=" + quality,
|
||||
function (data) {
|
||||
if (data && data.data && data.data.manifest) {
|
||||
try {
|
||||
var manifestStr = base64Decode(data.data.manifest);
|
||||
var manifest = JSON.parse(manifestStr);
|
||||
if (manifest.urls && manifest.urls.length > 0) {
|
||||
var streamUrl = manifest.urls[0];
|
||||
|
||||
// Unified Playback Strategy:
|
||||
// 1. Stop Everything
|
||||
stopAllAudio();
|
||||
|
||||
// 2. Try Native DOM Player (Visible Interface)
|
||||
// If this works, user gets controls. If it fails (IE), we fallback to SoundJS.
|
||||
playNativeFirst(streamUrl, id, quality);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Manifest error: " + e);
|
||||
}
|
||||
} else {
|
||||
handleError("Invalid track data");
|
||||
}
|
||||
},
|
||||
function (err) {
|
||||
handleError(err);
|
||||
}
|
||||
);
|
||||
|
||||
function playNativeFirst(url, id, quality, isRetry) {
|
||||
var domPlayer = $("#audio-player")[0];
|
||||
var playbackTimer = null;
|
||||
|
||||
// Quality Label
|
||||
var qLabel = (quality === "LOSSLESS") ? " (FLAC)" : " (AAC)";
|
||||
if (attemptFallback) qLabel = " (AAC)";
|
||||
|
||||
// Basic check for audio support
|
||||
if (domPlayer && typeof domPlayer.play === 'function') {
|
||||
|
||||
// Explicitly check for Codec support
|
||||
if (quality === "LOSSLESS") {
|
||||
// FLAC check
|
||||
var canPlay = "";
|
||||
try {
|
||||
canPlay = domPlayer.canPlayType("audio/flac");
|
||||
} catch(e) {}
|
||||
|
||||
if (canPlay === "" || canPlay === "no") {
|
||||
console.log("Browser reports no FLAC support. Fallback to AAC.");
|
||||
if (!attemptFallback) {
|
||||
window.playTrack(id, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus("Starting Native Playback" + qLabel + ((isRetry) ? " (HTTP)..." : "..."));
|
||||
|
||||
// Helper to handle retry vs legacy fallback
|
||||
function triggerRetryOrLegacy(msg) {
|
||||
if (playbackTimer) {
|
||||
clearTimeout(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
|
||||
// Try HTTP fallback if SSL failed
|
||||
if (!isRetry && url.indexOf("https://") === 0) {
|
||||
console.log("HTTPS failed/timeout (" + msg + "), retrying with HTTP...");
|
||||
var httpUrl = "http://" + url.substring(8);
|
||||
playNativeFirst(httpUrl, id, quality, true);
|
||||
return;
|
||||
}
|
||||
|
||||
playLegacySoundJS(streamUrl, id, quality);
|
||||
}
|
||||
|
||||
// Set error handler for THIS attempt
|
||||
domPlayer.onerror = function() {
|
||||
var errCode = domPlayer.error ? domPlayer.error.code : 0;
|
||||
console.log("Native Error Code: " + errCode);
|
||||
triggerRetryOrLegacy("onerror: " + errCode);
|
||||
};
|
||||
|
||||
try {
|
||||
domPlayer.src = url;
|
||||
domPlayer.preload = "auto";
|
||||
domPlayer.load(); // Force reload/buffering
|
||||
|
||||
var playPromise = domPlayer.play();
|
||||
|
||||
// Set a safety timeout for "forever pending" requests (common in Chrome 15 with SSL issues)
|
||||
playbackTimer = setTimeout(function() {
|
||||
console.log("Playback timeout - stalling detected.");
|
||||
triggerRetryOrLegacy("timeout");
|
||||
}, 5000); // 5 seconds to start playing
|
||||
|
||||
// If playback starts, clear timeout
|
||||
domPlayer.onplaying = function() {
|
||||
if (playbackTimer) {
|
||||
clearTimeout(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
updateStatus("Now Playing..." + qLabel);
|
||||
};
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(function() {
|
||||
// Promise resolved doesn't always mean playing started (buffering)
|
||||
// But usually it means intent is accepted.
|
||||
// We keep timer running until 'onplaying' checks in?
|
||||
// Actually promise resolve just means "accepted".
|
||||
// Chrome 15 won't have promise.
|
||||
// Modern browsers: resolve -> wait for data -> playing.
|
||||
// If data hangs, promise resolved but playing never fires.
|
||||
// So we keep timer.
|
||||
})
|
||||
.catch(function(e) {
|
||||
console.log("Native Play Promise Rejected: " + e.name);
|
||||
triggerRetryOrLegacy("promise rejection: " + e.name);
|
||||
});
|
||||
} else {
|
||||
// Legacy browser (no promise)
|
||||
// Wait for onplaying or timeout
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Native Exception: " + e.message);
|
||||
triggerRetryOrLegacy("exception: " + e.message);
|
||||
}
|
||||
|
||||
} else {
|
||||
// No native audio support (IE < 9)
|
||||
playLegacySoundJS(streamUrl, id, quality);
|
||||
}
|
||||
}
|
||||
|
||||
function playLegacySoundJS(url, id, quality, isRetry) {
|
||||
updateStatus("Activating Legacy Player (Flash)" + ((isRetry) ? " (HTTP)..." : "..."));
|
||||
|
||||
// SoundJS Logic
|
||||
var soundJsUrl = url;
|
||||
// Hint extension for SoundJS
|
||||
if (soundJsUrl.indexOf(".mp3") === -1 && soundJsUrl.indexOf(".m4a") === -1) {
|
||||
soundJsUrl += "#.m4a"; // Default to AAC hint
|
||||
}
|
||||
|
||||
// If FLAC and we are here, SoundJS will likely fail, but we'll try or alert.
|
||||
if (quality === "LOSSLESS") {
|
||||
// SoundJS can't do FLAC. And if native failed, we are out of luck for FLAC.
|
||||
// Try falling back to AAC quality for the whole track?
|
||||
if (!attemptFallback) {
|
||||
console.log("FLAC failed native, switching to HIGH quality fallback...");
|
||||
window.playTrack(id, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var soundId = "track_" + id + "_" + quality + (isRetry ? "_http" : "");
|
||||
createjs.Sound.removeAllEventListeners("fileload");
|
||||
|
||||
var playSound = function() {
|
||||
var instance = createjs.Sound.play(soundId);
|
||||
if (!instance || instance.playState === createjs.Sound.PLAY_FAILED) {
|
||||
handleLegacyError("Legacy Playback Failed");
|
||||
} else {
|
||||
updateStatus("Now Playing via Flash/Legacy...");
|
||||
}
|
||||
};
|
||||
|
||||
createjs.Sound.addEventListener("fileload", function(event) {
|
||||
if (event.id === soundId) {
|
||||
playSound();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
createjs.Sound.registerSound(soundJsUrl, soundId);
|
||||
} catch(e) {
|
||||
handleLegacyError("Legacy Setup Failed: " + e.message);
|
||||
}
|
||||
|
||||
function handleLegacyError(msg) {
|
||||
if (!isRetry && url.indexOf("https://") === 0) {
|
||||
console.log("Legacy HTTPS failed ("+msg+"), retrying HTTP...");
|
||||
var httpUrl = "http://" + url.substring(8);
|
||||
playLegacySoundJS(httpUrl, id, quality, true);
|
||||
return;
|
||||
}
|
||||
handleError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(msg) {
|
||||
if (currentTrackInfo.length) {
|
||||
currentTrackInfo.html(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(msg) {
|
||||
if (!attemptFallback) {
|
||||
window.playTrack(id, true);
|
||||
} else {
|
||||
// alert("Playback Error: " + (msg || "Unknown"));
|
||||
updateStatus("Error: " + msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function loadRecentTracks() {
|
||||
setContent("Loading recent tracks...");
|
||||
apiRequest(
|
||||
"/search/?s=a&limit=20",
|
||||
function (data) {
|
||||
if (data && data.data && data.data.items) {
|
||||
renderTracks(data.data.items, "Recently Added / Popular");
|
||||
} else {
|
||||
setContent("No recent tracks found.");
|
||||
}
|
||||
},
|
||||
function (err) {
|
||||
setContent("Error loading tracks: " + err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function fetchInstances(callback) {
|
||||
// using $.ajax directly to handle errors robustly
|
||||
$.ajax({
|
||||
url: "/instances.json",
|
||||
dataType: "json",
|
||||
success: function (instances) {
|
||||
if (instances && instances.length > 0) {
|
||||
apiInstances = shuffleArray(instances);
|
||||
// Clean URLs
|
||||
for (var i = 0; i < apiInstances.length; i++) {
|
||||
if (apiInstances[i].charAt(apiInstances[i].length - 1) === "/") {
|
||||
apiInstances[i] = apiInstances[i].substring(0, apiInstances[i].length - 1);
|
||||
}
|
||||
}
|
||||
currentInstanceIndex = 0;
|
||||
callback();
|
||||
} else {
|
||||
useFallback(callback);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
useFallback(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useFallback(callback) {
|
||||
apiInstances = shuffleArray(FALLBACK_INSTANCES.slice()); // Copy and shuffle
|
||||
currentInstanceIndex = 0;
|
||||
callback();
|
||||
}
|
||||
|
||||
function shuffleArray(array) {
|
||||
for (var i = array.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function performSearch(query) {
|
||||
var resultsDiv = $("#search-results");
|
||||
if (resultsDiv.length === 0) {
|
||||
setContent('<div id="search-results">Searching...</div>');
|
||||
resultsDiv = $("#search-results");
|
||||
} else {
|
||||
resultsDiv.html("Searching...");
|
||||
}
|
||||
|
||||
apiRequest(
|
||||
"/search/?s=" + encodeURIComponent(query) + "&limit=25",
|
||||
function (data) {
|
||||
var tracks = (data && data.data && data.data.items) ? data.data.items : [];
|
||||
if (tracks.length === 0) {
|
||||
resultsDiv.html("No results found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var html =
|
||||
'<table width="100%" border="1" cellpadding="2" cellspacing="0">';
|
||||
html +=
|
||||
'<tr bgcolor="#bbbbbb"><th>Play</th><th>Title</th><th>Artist</th><th>Album</th></tr>';
|
||||
|
||||
$.each(tracks, function (i, t) {
|
||||
var safeTitle = escapeHtml(t.title);
|
||||
var safeArtist = escapeHtml(t.artist.name);
|
||||
var safeAlbum = escapeHtml(t.album.title);
|
||||
|
||||
html += '<tr class="track-row">';
|
||||
html +=
|
||||
'<td align="center"><button onclick="window.playTrack(\'' +
|
||||
t.id +
|
||||
"')\">Play</button></td>";
|
||||
html += "<td>" + safeTitle + "</td>";
|
||||
html += "<td>" + safeArtist + "</td>";
|
||||
html += "<td>" + safeAlbum + "</td>";
|
||||
html += "</tr>";
|
||||
});
|
||||
html += "</table>";
|
||||
|
||||
resultsDiv.html(html);
|
||||
},
|
||||
function (err) {
|
||||
resultsDiv.html("Error: " + err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function renderTracks(tracks, title) {
|
||||
var html = "<h3>" + title + "</h3>";
|
||||
html += '<table width="100%" border="1" cellpadding="2" cellspacing="0">';
|
||||
html +=
|
||||
'<tr bgcolor="#bbbbbb"><th>Play</th><th>Title</th><th>Artist</th><th>Album</th></tr>';
|
||||
|
||||
$.each(tracks, function (i, t) {
|
||||
var safeTitle = escapeHtml(t.title);
|
||||
var safeArtist = escapeHtml(t.artist.name);
|
||||
var safeAlbum = escapeHtml(t.album.title);
|
||||
|
||||
html += '<tr class="track-row">';
|
||||
html +=
|
||||
'<td align="center"><button onclick="window.playTrack(\'' +
|
||||
t.id +
|
||||
"')\">Play</button></td>";
|
||||
html += "<td>" + safeTitle + "</td>";
|
||||
html += "<td>" + safeArtist + "</td>";
|
||||
html += "<td>" + safeAlbum + "</td>";
|
||||
html += "</tr>";
|
||||
});
|
||||
html += "</table>";
|
||||
|
||||
setContent(html);
|
||||
}
|
||||
|
||||
function setContent(html) {
|
||||
$("#main-content").html(html);
|
||||
}
|
||||
|
||||
function apiRequest(endpoint, success, error) {
|
||||
if (apiInstances.length === 0) {
|
||||
error("No API instances available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentBaseUrl = apiInstances[currentInstanceIndex];
|
||||
var finalUrl = currentBaseUrl;
|
||||
|
||||
// Check for HTTP fallback
|
||||
if (isHttpFallback) {
|
||||
// If original was https, downgrade it
|
||||
if (finalUrl.indexOf("https://") === 0) {
|
||||
finalUrl = "http://" + finalUrl.substring(8);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$.ajax({
|
||||
url: finalUrl + endpoint,
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
// Logic: If successful with HTTP fallback, maybe we should stick to it?
|
||||
// For now, we just proceed.
|
||||
success(data);
|
||||
},
|
||||
error: function (xhr, status, errorThrown) {
|
||||
handleApiError(endpoint, success, error, status + " (" + errorThrown + ")");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
handleApiError(endpoint, success, error, "Exception: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function handleApiError(endpoint, success, error, errorMsg) {
|
||||
// 1. Try HTTP fallback for current instance if allowed
|
||||
if (!isHttpFallback && window.location.protocol !== "https:") {
|
||||
// Only if current instance is HTTPS
|
||||
if (apiInstances[currentInstanceIndex].indexOf("https://") === 0) {
|
||||
isHttpFallback = true;
|
||||
apiRequest(endpoint, success, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Move to next instance
|
||||
isHttpFallback = false; // Reset for next instance
|
||||
currentInstanceIndex++;
|
||||
|
||||
if (currentInstanceIndex < apiInstances.length) {
|
||||
// Retry with next instance
|
||||
apiRequest(endpoint, success, error);
|
||||
} else {
|
||||
// All instances failed
|
||||
// We could try to reset index and wait, but for now we fail.
|
||||
// Or maybe we should loop back to 0? But infinite loops are bad.
|
||||
// Let's just fail after one full rotation.
|
||||
currentInstanceIndex = 0; // Reset for next user interaction attempt
|
||||
error("All API instances failed. Last error: " + errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
function playWithEmbed(url) {
|
||||
var container = $("#audio-container");
|
||||
if (container.length === 0) {
|
||||
if (audioPlayer && audioPlayer.parentNode) {
|
||||
$(audioPlayer.parentNode).attr("id", "audio-container");
|
||||
container = $("#audio-container");
|
||||
}
|
||||
}
|
||||
|
||||
if (container.length) {
|
||||
var embedDiv = $("#embed-container");
|
||||
if (embedDiv.length === 0) {
|
||||
embedDiv = $('<div id="embed-container"></div>');
|
||||
container.append(embedDiv);
|
||||
}
|
||||
// Use html() to set innerHTML properly
|
||||
var embedHtml = '<embed type="application/x-mplayer2" src="' + url + '" autostart="true" width="0" height="0" enablejavascript="true"></embed>';
|
||||
embedDiv.html(embedHtml);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function base64Decode(str) {
|
||||
if (window.atob) {
|
||||
return window.atob(str);
|
||||
}
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
var output = "";
|
||||
str = String(str).replace(/=+$/, '');
|
||||
if (str.length % 4 == 1) {
|
||||
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
|
||||
}
|
||||
for (
|
||||
var bc = 0, bs = 0, buffer, i = 0;
|
||||
buffer = str.charAt(i++);
|
||||
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
|
||||
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
});
|
||||
18
legacy/soundjs-0.5.2.min.js
vendored
18
legacy/soundjs-0.5.2.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -8,6 +8,8 @@
|
|||
"https://katze.qqdl.site",
|
||||
"https://hund.qqdl.site",
|
||||
"https://tidal.kinoplus.online",
|
||||
"https://tidal-api.binimum.org"
|
||||
"https://tidal-api.binimum.org",
|
||||
"https://hifi-one.spotisaver.net",
|
||||
"https://hifi-two.spotisaver.net"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
115
self-hosted-database.md
Normal file
115
self-hosted-database.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
|
||||
This guide will show you how to setup the necessary stuff to be able to use your own authentication system and database for accounts. please do note that you will have to enter the same configurations for each device.
|
||||
|
||||
**This Guide Assumes You're Doing everything On Your Local Machine. This is still fully possible on a VPS Though.**
|
||||
|
||||
### Required:
|
||||
- A Computer (this computer will be the one hosting the database)
|
||||
- Firebase Account (Only Used For Authentication)
|
||||
- [PocketBase](https://pocketbase.io) (App we use to manage The Database, Install This on the computer you want to host the database on)
|
||||
- Domain (you can get one for free at [DigitalPlat](https://domain.digitalplat.org/))
|
||||
|
||||
|
||||
### Step 1: Setup Firebase Authentication
|
||||
Go to the [Firebase Console](https://console.firebase.google.com) and create a new project. then, on the left sidebar, click the **Build** section and select **Authentication**.
|
||||
|
||||
1. Click **Get Started**.
|
||||
2. Go to the **Sign-in method** tab.
|
||||
3. Select **Google** And **Email** and enable it.
|
||||
4. Set your project support email and click **Save**.
|
||||
|
||||
### Step 1.1: Authorize The Domain
|
||||
firebase by default makes you add trusted domains to connect to firebases authentication system, if your domain isnt on there, it wont allow you to login or signup.
|
||||
|
||||
1. In the **Authentication** section, go to the **Settings** tab.
|
||||
2. Click **Authorized domains** in the left sub-menu.
|
||||
3. Click **Add domain**.
|
||||
4. Add your hosting domain (if you're still using the normal monochrome site, just add `monochrome.samidy.com`, or whatever mirror you like to use (eg. `monochrome.tf`). otherwise, if you're self hosting the website too, add your hosted domain).
|
||||
- _Note: `localhost` is usually added by default for local testing. you likely wont have people abusing your system, so you can leave this in by default._
|
||||
|
||||
### Step 2: PocketBase Setup
|
||||
1. download [PocketBase](https://pocketbase.io) and follow their setup guide.
|
||||
2. make 2 collections: `DB_users` and `public_playlists`. do NOT use the normal "users" collection.
|
||||
3. Add these fields to `DB_users`:
|
||||
- name: `firebase_id` type: `Plain Text`
|
||||
- name: `lastUpdated` type: `Number`
|
||||
- name: `history` type: `JSON`
|
||||
- name: `library` type: `JSON`
|
||||
- name: `user_playlists` type: `JSON`
|
||||
- name: `deleted_playlists` type: `JSON`
|
||||
4. Add these fields to `public_playlists`:
|
||||
- name: `firebase_id` type: `Plain Text`
|
||||
- name: `addedAt` type: `Number`
|
||||
- name: `numberOfTracks` type: `Number`
|
||||
- name: `OriginalId` type: `Plain Text`
|
||||
- name: `publishedAt` type: `Number`
|
||||
- name: `title` type: `Plain Text`
|
||||
- name: `uid` type: `Plain Text`
|
||||
- name: `uuid` type: `Plain Text`
|
||||
- name: `tracks` type: `JSON`
|
||||
- name: `image` type: `URL`
|
||||
5. edit the `API Rules` for both `DB_users` and `public_playlists` to these:
|
||||
|
||||
#### `DB_users`
|
||||

|
||||
|
||||
#### `public_playlists`
|
||||

|
||||
|
||||
|
||||
Now that you have setup collections, rules and fields, we can now work on putting them out on the internet.
|
||||
|
||||
|
||||
### Step 3: Cloudflared
|
||||
while you can use the usual `127.0.0.1` link pocketbase gives you, this is a local domain and you cant enter it on any other device, so it would practically be useless. to open this up, while we can port forward, this could be dangerous and attackers could use that as a vulnerability. to securely set this up, we are going to be using cloudflared.
|
||||
|
||||
1. Make an account at the [Cloudflare Dashboard](https://dash.cloudflare.com).
|
||||
2. In the Left Sidebar, Click "Zero Trust".
|
||||
3. Set Up Zero Trust, then in the left sidebar, Click "Networks" Then "connectors".
|
||||
4. Select "Cloudflared".
|
||||
5. Give Your Tunnel A Name (eg. `My very cool monochrome database wow im so cool`)
|
||||
6. then, you will get a guide on how to install cloudflared and set it up for your machine.
|
||||
7. You will get a window to setup hostnames, Note that you will require a valid domain as cloudflare doesnt allow `pages.dev` domains. you can get one for free at [DigitalPlat](https://domain.digitalplat.org/), but we will not show you how to set it up and how to connect it to cloudflare.
|
||||
8. at the "Service" section for the setup hostnames window, select "HTTP" and input the URL for pocketbase (eg. `127.0.0.1:8090`).
|
||||
after this, your database will be available at the chosen domain.
|
||||
|
||||
|
||||
|
||||
### Step 4: Getting Configurations
|
||||
You are almost done, now you just need to get configurations so you can add them to monochrome.
|
||||
|
||||
first, get your authentication config:
|
||||
|
||||
1. Open Firebase Console and click on your project.
|
||||
2. click the Gear Icon (⚙️) next to "Project Overview" and select **Project settings**.
|
||||
3. In the **General** tab, scroll down to "Your apps" and click the **Web icon (`</>`)**.
|
||||
4. Register the app (e.g., "Monochrome Auth").
|
||||
5. You will see a `firebaseConfig` object. It looks like this:
|
||||
```
|
||||
const firebaseConfig = {
|
||||
apiKey: 'AIzaSy...',
|
||||
authDomain: 'your-project.firebaseapp.com',
|
||||
databaseURL: 'https://your-project.firebaseio.com',
|
||||
projectId: 'your-project',
|
||||
storageBucket: 'your-project.appspot.com',
|
||||
messagingSenderId: '...',
|
||||
appId: '...',
|
||||
};
|
||||
```
|
||||
6. **Copy only the part with the curly braces `{ ... }`**.
|
||||
|
||||
For The Database:
|
||||
just copy the link for your database.
|
||||
|
||||
|
||||
|
||||
### Step 5: Linking with monochrome
|
||||
now all you need to do is add your configurations in monochrome.
|
||||
|
||||
1. Go to settings in monochrome.
|
||||
2. Click "ADVANCED: Custom Account Database"
|
||||
3. in the database Config input window, input your database link.
|
||||
4. in the authentication config input window, input the JSON object you got from firebase.
|
||||
5. Click "Save"
|
||||
|
||||
Thats it! you now have setup a custom authentication system and database system. do note, on every device you wanna use your custom database on, you will have to repeat step 5 on the given device.
|
||||
Loading…
Reference in a new issue