Merge pull request #352 from DanTheMan827/minor-fixes

A collection of fixes
This commit is contained in:
edidealt 2026-03-19 22:21:53 +02:00 committed by GitHub
commit fc4adfcd32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 187 additions and 113 deletions

View file

@ -8,7 +8,7 @@ on:
permissions: permissions:
contents: write contents: write
workflows: write actions: write
jobs: jobs:
lint: lint:

View file

@ -4706,8 +4706,8 @@
<span class="description">Quality for streaming playback</span> <span class="description">Quality for streaming playback</span>
</div> </div>
<select id="streaming-quality-setting"> <select id="streaming-quality-setting">
<option value="HI_RES_LOSSLESS">Hi-Res FLAC (24-bit)</option> <option value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit)</option>
<option value="LOSSLESS">FLAC (Lossless)</option> <option value="LOSSLESS">Lossless (16-bit)</option>
<option value="HIGH">AAC 320kbps</option> <option value="HIGH">AAC 320kbps</option>
<option value="LOW">AAC 96kbps</option> <option value="LOW">AAC 96kbps</option>
</select> </select>
@ -5150,18 +5150,28 @@
<span class="slider"></span> <span class="slider"></span>
</label> </label>
</div> </div>
<div>
<div class="setting-item"> <div class="setting-item">
<div class="info"> <div class="info">
<span class="label">Download Quality</span> <span class="label">Download Quality</span>
<span class="description">Quality for track downloads</span> <span class="description">Quality for track downloads</span>
</div> </div>
<select id="download-quality-setting"> <select id="download-quality-setting">
<option value="HI_RES_LOSSLESS">Hi-Res FLAC (24-bit)</option> <option value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit)</option>
<option value="LOSSLESS">FLAC (Lossless)</option> <option value="LOSSLESS">Lossless (16-bit)</option>
<option value="HIGH">AAC 320kbps</option> <option value="HIGH">AAC 320kbps</option>
<option value="LOW">AAC 96kbps</option> <option value="LOW">AAC 96kbps</option>
</select> </select>
</div> </div>
<div class="setting-item" id="hi-res-download-warning" style="display: none">
<div class="info setting-details">
<span class="description">
24-bit downloads may crash the browser on some devices, or be missing
metadata.
</span>
</div>
</div>
</div>
<div class="setting-item"> <div class="setting-item">
<div class="info"> <div class="info">
<span class="label">Lossless Container</span> <span class="label">Lossless Container</span>

6
js/METADATA_STRINGS.js Normal file
View file

@ -0,0 +1,6 @@
export const METADATA_STRINGS = {
VENDOR_STRING: 'Monochrome',
DEFAULT_TITLE: 'Unknown Title',
DEFAULT_ARTIST: 'Unknown Artist',
DEFAULT_ALBUM: 'Unknown Album',
};

View file

@ -21,6 +21,7 @@ import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts';
import { isCustomFormat } from './ffmpegFormats.ts'; import { isCustomFormat } from './ffmpegFormats.ts';
import { DownloadProgress } from './progressEvents.js'; import { DownloadProgress } from './progressEvents.js';
import { resolveDownloadTotalBytes } from './downloadProgressUtils.js'; import { resolveDownloadTotalBytes } from './downloadProgressUtils.js';
import { readableStreamIterator } from './readableStreamIterator.js';
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
export { resolveDownloadTotalBytes }; export { resolveDownloadTotalBytes };
@ -1431,20 +1432,14 @@ export class LosslessAPI {
let receivedBytes = 0; let receivedBytes = 0;
if (response.body) { if (response.body) {
const reader = response.body.getReader();
const chunks = []; const chunks = [];
while (true) { for await (const chunk of readableStreamIterator(response.body)) {
const { done, value } = await reader.read(); chunks.push(chunk);
if (done) break; receivedBytes += chunk.byteLength;
if (value) {
chunks.push(value);
receivedBytes += value.byteLength;
onProgress?.(new DownloadProgress(receivedBytes, totalBytes || undefined)); onProgress?.(new DownloadProgress(receivedBytes, totalBytes || undefined));
} }
}
const defaultMime = isVideo ? 'video/mp4' : 'audio/flac'; const defaultMime = isVideo ? 'video/mp4' : 'audio/flac';
blob = new Blob(chunks, { type: response.headers.get('Content-Type') || defaultMime }); blob = new Blob(chunks, { type: response.headers.get('Content-Type') || defaultMime });

View file

@ -1,4 +1,5 @@
//js/app.js //js/app.js
import { isIos, isSafari } from './platform-detection.js';
import { MusicAPI } from './music-api.js'; import { MusicAPI } from './music-api.js';
import { import {
apiSettings, apiSettings,
@ -28,7 +29,6 @@ import { registerSW } from 'virtual:pwa-register';
import { openEditProfile } from './profile.js'; import { openEditProfile } from './profile.js';
import { ThemeStore } from './themeStore.js'; import { ThemeStore } from './themeStore.js';
import './commandPalette.js'; import './commandPalette.js';
import { initTracker } from './tracker.js'; import { initTracker } from './tracker.js';
import { import {
initAnalytics, initAnalytics,
@ -65,8 +65,6 @@ import {
// Capture real iOS state before spoofing (needed for background audio) // Capture real iOS state before spoofing (needed for background audio)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const _ua = navigator.userAgent.toLowerCase(); const _ua = navigator.userAgent.toLowerCase();
window.__IS_IOS__ = /iphone|ipad|ipod/.test(_ua) || (_ua.includes('mac') && navigator.maxTouchPoints > 1);
// Spoof User-Agent to bypass Google's embedded browser check // Spoof User-Agent to bypass Google's embedded browser check
Object.defineProperty(navigator, 'userAgent', { Object.defineProperty(navigator, 'userAgent', {
get: function () { get: function () {
@ -387,16 +385,11 @@ document.addEventListener('DOMContentLoaded', async () => {
const api = new MusicAPI(apiSettings); const api = new MusicAPI(apiSettings);
const audioPlayer = document.getElementById('audio-player'); const audioPlayer = document.getElementById('audio-player');
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only // i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback
// Use window.__IS_IOS__ (set before UA spoof in index.html) so detection works on real iOS. // Use isIos from platform-detection (set before UA spoof in index.html) so detection works on real iOS.
const isIOS = typeof window !== 'undefined' && window.__IS_IOS__ === true; if (isIos || isSafari) {
const ua = navigator.userAgent.toLowerCase();
const isSafari =
ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android');
if (isIOS || isSafari) {
const qualitySelect = document.getElementById('streaming-quality-setting'); const qualitySelect = document.getElementById('streaming-quality-setting');
const downloadSelect = document.getElementById('download-quality-setting'); const downloadQualitySelect = document.getElementById('download-quality-setting');
const removeHiRes = (select) => { const removeHiRes = (select) => {
if (!select) return; if (!select) return;
@ -405,7 +398,10 @@ document.addEventListener('DOMContentLoaded', async () => {
}; };
removeHiRes(qualitySelect); removeHiRes(qualitySelect);
removeHiRes(downloadSelect);
if (isIos) {
document.querySelector('#hi-res-download-warning').style.display = '';
}
const currentQualitySetting = localStorage.getItem('playback-quality'); const currentQualitySetting = localStorage.getItem('playback-quality');
if (!currentQualitySetting || currentQualitySetting === 'HI_RES_LOSSLESS') { if (!currentQualitySetting || currentQualitySetting === 'HI_RES_LOSSLESS') {

View file

@ -2,6 +2,7 @@
// Shared Audio Context Manager - handles EQ and provides context for visualizer // Shared Audio Context Manager - handles EQ and provides context for visualizer
// Supports 3-32 parametric EQ bands // Supports 3-32 parametric EQ bands
import { isIos } from './platform-detection.js';
import { equalizerSettings, monoAudioSettings } from './storage.js'; import { equalizerSettings, monoAudioSettings } from './storage.js';
// Generate frequency array for given number of bands using logarithmic spacing // Generate frequency array for given number of bands using logarithmic spacing
@ -300,8 +301,7 @@ class AudioContextManager {
this.audio = audioElement; this.audio = audioElement;
// Detect iOS - skip Web Audio initialization on iOS to avoid lock screen audio issues // Detect iOS - skip Web Audio initialization on iOS to avoid lock screen audio issues
const isIOS = typeof window !== 'undefined' && window.__IS_IOS__ === true; if (isIos) {
if (isIOS) {
console.log('[AudioContext] Skipping Web Audio initialization on iOS for lock screen compatibility'); console.log('[AudioContext] Skipping Web Audio initialization on iOS for lock screen compatibility');
return; return;
} }

View file

@ -135,9 +135,7 @@ export class ZipNeutralinoWriter implements IBulkDownloadWriter {
const reader = response.body.getReader(); const reader = response.body.getReader();
let receivedLength = 0; let receivedLength = 0;
while (true) { for await (const value of readableStreamIterator(response.body)) {
const { done, value } = await reader.read();
if (done) break;
const chunk = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); const chunk = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
await bridge.filesystem.appendBinaryFile(savePath, chunk); await bridge.filesystem.appendBinaryFile(savePath, chunk);
receivedLength += value.length; receivedLength += value.length;

View file

@ -4,6 +4,7 @@ import { v7 } from 'uuid';
export const InvisibleCodec = baseCodecFrom(InvisibleDictionary); export const InvisibleCodec = baseCodecFrom(InvisibleDictionary);
export function doTimed<T>(message: string, callback: () => T): T { export function doTimed<T>(message: string, callback: () => T): T {
if (import.meta.env.DEV) {
const hiddenId = InvisibleCodec.encode(v7()); const hiddenId = InvisibleCodec.encode(v7());
console.time(message + hiddenId); console.time(message + hiddenId);
try { try {
@ -12,15 +13,35 @@ export function doTimed<T>(message: string, callback: () => T): T {
} finally { } finally {
console.timeEnd(message + hiddenId); console.timeEnd(message + hiddenId);
} }
} else {
return callback();
}
} }
export async function doTimedAsync<T>(message: string, callback: () => T): Promise<Awaited<T>> { export function doTimedAsync<T, R = T extends Promise<T> ? Promise<T> : T>(
message: string,
callback: () => R,
throwError: boolean = false
): R {
if (import.meta.env.DEV) {
return new Promise(async (resolve, reject) => {
const hiddenId = InvisibleCodec.encode(v7()); const hiddenId = InvisibleCodec.encode(v7());
console.time(message + hiddenId); console.time(message + hiddenId);
try { try {
const output = await callback(); const output = await callback();
return output; resolve(output);
} catch (err) {
console.error(`Error in timed operation "${message}":`, err);
if (throwError) {
reject(err);
} else {
resolve(undefined as R);
}
} finally { } finally {
console.timeEnd(message + hiddenId); console.timeEnd(message + hiddenId);
} }
}) as R;
} else {
return callback() as R;
}
} }

View file

@ -838,7 +838,7 @@ function updateBulkDownloadProgress(notifEl, current, total, currentItem, progre
const percent = progress.progress || 0; const percent = progress.progress || 0;
progressFill.style.width = `${percent}%`; progressFill.style.width = `${percent}%`;
progressFill.style.background = '#3b82f6'; // Blue for encoding progressFill.style.background = '#3b82f6'; // Blue for encoding
statusEl.textContent = `Converting ${Math.ceil(current)}/${total}: ${Math.round(percent)}%`; statusEl.textContent = `Converting ${Math.floor(current + 1)}/${total}: ${Math.round(percent)}%`;
return; return;
} }
@ -849,7 +849,7 @@ function updateBulkDownloadProgress(notifEl, current, total, currentItem, progre
const percent = total > 0 ? Math.round((current / total) * 100) : 0; const percent = total > 0 ? Math.round((current / total) * 100) : 0;
progressFill.style.width = `${percent}%`; progressFill.style.width = `${percent}%`;
progressFill.style.background = 'var(--highlight)'; progressFill.style.background = 'var(--highlight)';
statusEl.textContent = `${Math.floor(current)}/${total} - ${currentItem}`; statusEl.textContent = `${Math.floor(current + 1)}/${total} - ${currentItem}`;
} }
function completeBulkDownload(notifEl, success = true, message = null) { function completeBulkDownload(notifEl, success = true, message = null) {

View file

@ -1,6 +1,6 @@
import { getCoverBlob, getTrackTitle } from './utils.js'; import { getCoverBlob, getTrackTitle } from './utils.js';
import { getFullArtistString } from './utils.js'; import { getFullArtistString } from './utils.js';
import { METADATA_STRINGS } from './metadata.js'; import { METADATA_STRINGS } from './METADATA_STRINGS.js';
export const FLAC_MIME_TYPE = 'audio/flac'; export const FLAC_MIME_TYPE = 'audio/flac';
const FLAC_BLOCK_TYPES = { const FLAC_BLOCK_TYPES = {

View file

@ -11,13 +11,6 @@ import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './tag
import { doTimed, doTimedAsync } from './doTimed.ts'; import { doTimed, doTimedAsync } from './doTimed.ts';
import { managers } from './app.js'; import { managers } from './app.js';
export const METADATA_STRINGS = {
VENDOR_STRING: 'Monochrome',
DEFAULT_TITLE: 'Unknown Title',
DEFAULT_ARTIST: 'Unknown Artist',
DEFAULT_ALBUM: 'Unknown Album',
};
export function prefetchMetadataObjects(track, api, coverBlob = null) { export function prefetchMetadataObjects(track, api, coverBlob = null) {
const _tagLib = fetchTagLib().catch(console.error); const _tagLib = fetchTagLib().catch(console.error);
const coverId = getTrackCoverId(track); const coverId = getTrackCoverId(track);

17
js/platform-detection.ts Normal file
View file

@ -0,0 +1,17 @@
/** The original user agent string before spoofing. */
export const originalUserAgent = navigator.userAgent;
/** A lowercase version of the original user agent string. */
const lowerCaseOriginalUserAgent = originalUserAgent.toLowerCase();
/** If the device is an iOS device. (iPhone, iPad, iPod, or Apple Vision) */
export const isIos =
/iphone|ipad|ipod|applevision/.test(lowerCaseOriginalUserAgent) ||
(lowerCaseOriginalUserAgent.includes('mac') && navigator.maxTouchPoints > 1);
/** If the browser is Safari (excluding Chrome, Chromium-based browsers, and Android browsers). */
export const isSafari =
lowerCaseOriginalUserAgent.includes('safari') &&
!lowerCaseOriginalUserAgent.includes('chrome') &&
!lowerCaseOriginalUserAgent.includes('crios') &&
!lowerCaseOriginalUserAgent.includes('android');

View file

@ -21,6 +21,7 @@ import {
import { audioContextManager } from './audio-context.js'; import { audioContextManager } from './audio-context.js';
import { db } from './db.js'; import { db } from './db.js';
import Hls from 'hls.js'; import Hls from 'hls.js';
import { isIos } from './platform-detection.js';
export class Player { export class Player {
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') { constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
@ -42,7 +43,7 @@ export class Player {
this.isFallbackRetry = false; this.isFallbackRetry = false;
this.isFallbackInProgress = false; this.isFallbackInProgress = false;
this.autoplayBlocked = false; this.autoplayBlocked = false;
this.isIOS = typeof window !== 'undefined' && window.__IS_IOS__ === true; this.isIOS = isIos;
this.isPwa = this.isPwa =
typeof window !== 'undefined' && typeof window !== 'undefined' &&
(window.matchMedia?.('(display-mode: standalone)')?.matches || window.navigator?.standalone === true); (window.matchMedia?.('(display-mode: standalone)')?.matches || window.navigator?.standalone === true);

View file

@ -0,0 +1,28 @@
/**
* Converts a ReadableStream into an async iterable iterator.
* @template T The type of data chunks yielded from the stream.
* @param stream The ReadableStream to convert into an async iterable.
* @yields Chunks of data from the stream as they become available.
* @example
* ```typescript
* const response = await fetch('https://example.com/data');
* for await (const chunk of readableStreamIterator(response.body)) {
* console.log(chunk);
* }
* ```
*/
export async function* readableStreamIterator<T>(stream: ReadableStream<T>): AsyncIterableIterator<T> {
const reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (value) {
yield value;
}
if (done) {
break;
}
}
}

View file

@ -830,6 +830,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
}; };
const categoryOrder = ['Lossless', 'AAC', 'MP3', 'OGG']; const categoryOrder = ['Lossless', 'AAC', 'MP3', 'OGG'];
allOptions.sort((a, b) => { allOptions.sort((a, b) => {
if (a.category == b.category && a.category === 'Lossless') return 0; // Preserve original order for lossless options
const ai = categoryOrder.indexOf(a.category); const ai = categoryOrder.indexOf(a.category);
const bi = categoryOrder.indexOf(b.category); const bi = categoryOrder.indexOf(b.category);
const categoryDiff = (ai === -1 ? categoryOrder.length : ai) - (bi === -1 ? categoryOrder.length : bi); const categoryDiff = (ai === -1 ? categoryOrder.length : ai) - (bi === -1 ? categoryOrder.length : bi);
@ -3155,7 +3156,6 @@ export function initializeSettings(scrobbler, player, api, ui) {
// Store might not exist, continue // Store might not exist, continue
} }
} }
} catch (dbError) { } catch (dbError) {
console.log('Could not clear IndexedDB stores:', dbError); console.log('Could not clear IndexedDB stores:', dbError);
// Try to delete the entire database as fallback // Try to delete the entire database as fallback

View file

@ -2737,6 +2737,15 @@ input[type='search']::-webkit-search-cancel-button {
color: var(--muted-foreground); color: var(--muted-foreground);
} }
.setting-item .info.setting-details {
width: 100%;
display: block;
}
.setting-item .info.setting-details .description {
font-size: 0.8rem;
}
.setting-item select, .setting-item select,
.setting-item input[type='number'] { .setting-item input[type='number'] {
background-color: var(--input); background-color: var(--input);
@ -4936,7 +4945,6 @@ input:checked + .slider::before {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.customize-shortcut-item { .customize-shortcut-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -38,6 +38,7 @@ export default defineConfig(({ mode }) => {
build: { build: {
outDir: 'dist', outDir: 'dist',
emptyOutDir: true, emptyOutDir: true,
sourcemap: true,
}, },
plugins: [ plugins: [
IS_NEUTRALINO && neutralino(), IS_NEUTRALINO && neutralino(),