Merge pull request #352 from DanTheMan827/minor-fixes
A collection of fixes
This commit is contained in:
commit
fc4adfcd32
17 changed files with 187 additions and 113 deletions
90
.github/workflows/lint.yml
vendored
90
.github/workflows/lint.yml
vendored
|
|
@ -1,61 +1,61 @@
|
|||
name: Lint Codebase
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
workflows: write
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./bun_modules
|
||||
./node_modules
|
||||
./bun.lock
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./bun_modules
|
||||
./node_modules
|
||||
./bun.lock
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run JS Lint
|
||||
run: bun run lint:js -- --fix
|
||||
continue-on-error: true
|
||||
- name: Run JS Lint
|
||||
run: bun run lint:js -- --fix
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run CSS Lint
|
||||
run: bun run lint:css -- --fix
|
||||
continue-on-error: true
|
||||
- name: Run CSS Lint
|
||||
run: bun run lint:css -- --fix
|
||||
continue-on-error: true
|
||||
|
||||
- name: Format with Prettier
|
||||
run: bun run format
|
||||
continue-on-error: true
|
||||
- name: Format with Prettier
|
||||
run: bun run format
|
||||
continue-on-error: true
|
||||
|
||||
- name: Commit and Push lint fixes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: 'style: auto-fix linting issues'
|
||||
commit_user_name: 'github-actions[bot]'
|
||||
commit_user_email: 'github-actions[bot]@users.noreply.github.com'
|
||||
only_if_changed: true
|
||||
- name: Commit and Push lint fixes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: 'style: auto-fix linting issues'
|
||||
commit_user_name: 'github-actions[bot]'
|
||||
commit_user_email: 'github-actions[bot]@users.noreply.github.com'
|
||||
only_if_changed: true
|
||||
|
||||
- name: Run HTML Lint
|
||||
run: bun run lint:html
|
||||
- name: Run HTML Lint
|
||||
run: bun run lint:html
|
||||
|
|
|
|||
34
index.html
34
index.html
|
|
@ -4706,8 +4706,8 @@
|
|||
<span class="description">Quality for streaming playback</span>
|
||||
</div>
|
||||
<select id="streaming-quality-setting">
|
||||
<option value="HI_RES_LOSSLESS">Hi-Res FLAC (24-bit)</option>
|
||||
<option value="LOSSLESS">FLAC (Lossless)</option>
|
||||
<option value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit)</option>
|
||||
<option value="LOSSLESS">Lossless (16-bit)</option>
|
||||
<option value="HIGH">AAC 320kbps</option>
|
||||
<option value="LOW">AAC 96kbps</option>
|
||||
</select>
|
||||
|
|
@ -5150,17 +5150,27 @@
|
|||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Download Quality</span>
|
||||
<span class="description">Quality for track downloads</span>
|
||||
<div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Download Quality</span>
|
||||
<span class="description">Quality for track downloads</span>
|
||||
</div>
|
||||
<select id="download-quality-setting">
|
||||
<option value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit)</option>
|
||||
<option value="LOSSLESS">Lossless (16-bit)</option>
|
||||
<option value="HIGH">AAC 320kbps</option>
|
||||
<option value="LOW">AAC 96kbps</option>
|
||||
</select>
|
||||
</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>
|
||||
<select id="download-quality-setting">
|
||||
<option value="HI_RES_LOSSLESS">Hi-Res FLAC (24-bit)</option>
|
||||
<option value="LOSSLESS">FLAC (Lossless)</option>
|
||||
<option value="HIGH">AAC 320kbps</option>
|
||||
<option value="LOW">AAC 96kbps</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
|
|
|
|||
6
js/METADATA_STRINGS.js
Normal file
6
js/METADATA_STRINGS.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const METADATA_STRINGS = {
|
||||
VENDOR_STRING: 'Monochrome',
|
||||
DEFAULT_TITLE: 'Unknown Title',
|
||||
DEFAULT_ARTIST: 'Unknown Artist',
|
||||
DEFAULT_ALBUM: 'Unknown Album',
|
||||
};
|
||||
15
js/api.js
15
js/api.js
|
|
@ -21,6 +21,7 @@ import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts';
|
|||
import { isCustomFormat } from './ffmpegFormats.ts';
|
||||
import { DownloadProgress } from './progressEvents.js';
|
||||
import { resolveDownloadTotalBytes } from './downloadProgressUtils.js';
|
||||
import { readableStreamIterator } from './readableStreamIterator.js';
|
||||
|
||||
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
|
||||
export { resolveDownloadTotalBytes };
|
||||
|
|
@ -1431,19 +1432,13 @@ export class LosslessAPI {
|
|||
let receivedBytes = 0;
|
||||
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
const chunks = [];
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
for await (const chunk of readableStreamIterator(response.body)) {
|
||||
chunks.push(chunk);
|
||||
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';
|
||||
|
|
|
|||
22
js/app.js
22
js/app.js
|
|
@ -1,4 +1,5 @@
|
|||
//js/app.js
|
||||
import { isIos, isSafari } from './platform-detection.js';
|
||||
import { MusicAPI } from './music-api.js';
|
||||
import {
|
||||
apiSettings,
|
||||
|
|
@ -28,7 +29,6 @@ import { registerSW } from 'virtual:pwa-register';
|
|||
import { openEditProfile } from './profile.js';
|
||||
import { ThemeStore } from './themeStore.js';
|
||||
import './commandPalette.js';
|
||||
|
||||
import { initTracker } from './tracker.js';
|
||||
import {
|
||||
initAnalytics,
|
||||
|
|
@ -65,8 +65,6 @@ import {
|
|||
// Capture real iOS state before spoofing (needed for background audio)
|
||||
if (typeof window !== 'undefined') {
|
||||
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
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
get: function () {
|
||||
|
|
@ -387,16 +385,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const api = new MusicAPI(apiSettings);
|
||||
const audioPlayer = document.getElementById('audio-player');
|
||||
|
||||
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only
|
||||
// Use window.__IS_IOS__ (set before UA spoof in index.html) so detection works on real iOS.
|
||||
const isIOS = typeof window !== 'undefined' && window.__IS_IOS__ === true;
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isSafari =
|
||||
ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android');
|
||||
|
||||
if (isIOS || isSafari) {
|
||||
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback
|
||||
// Use isIos from platform-detection (set before UA spoof in index.html) so detection works on real iOS.
|
||||
if (isIos || isSafari) {
|
||||
const qualitySelect = document.getElementById('streaming-quality-setting');
|
||||
const downloadSelect = document.getElementById('download-quality-setting');
|
||||
const downloadQualitySelect = document.getElementById('download-quality-setting');
|
||||
|
||||
const removeHiRes = (select) => {
|
||||
if (!select) return;
|
||||
|
|
@ -405,7 +398,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
};
|
||||
|
||||
removeHiRes(qualitySelect);
|
||||
removeHiRes(downloadSelect);
|
||||
|
||||
if (isIos) {
|
||||
document.querySelector('#hi-res-download-warning').style.display = '';
|
||||
}
|
||||
|
||||
const currentQualitySetting = localStorage.getItem('playback-quality');
|
||||
if (!currentQualitySetting || currentQualitySetting === 'HI_RES_LOSSLESS') {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Shared Audio Context Manager - handles EQ and provides context for visualizer
|
||||
// Supports 3-32 parametric EQ bands
|
||||
|
||||
import { isIos } from './platform-detection.js';
|
||||
import { equalizerSettings, monoAudioSettings } from './storage.js';
|
||||
|
||||
// Generate frequency array for given number of bands using logarithmic spacing
|
||||
|
|
@ -300,8 +301,7 @@ class AudioContextManager {
|
|||
this.audio = audioElement;
|
||||
|
||||
// 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');
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,9 +135,7 @@ export class ZipNeutralinoWriter implements IBulkDownloadWriter {
|
|||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
for await (const value of readableStreamIterator(response.body)) {
|
||||
const chunk = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
||||
await bridge.filesystem.appendBinaryFile(savePath, chunk);
|
||||
receivedLength += value.length;
|
||||
|
|
|
|||
|
|
@ -4,23 +4,44 @@ import { v7 } from 'uuid';
|
|||
export const InvisibleCodec = baseCodecFrom(InvisibleDictionary);
|
||||
|
||||
export function doTimed<T>(message: string, callback: () => T): T {
|
||||
const hiddenId = InvisibleCodec.encode(v7());
|
||||
console.time(message + hiddenId);
|
||||
try {
|
||||
const output = callback();
|
||||
return output;
|
||||
} finally {
|
||||
console.timeEnd(message + hiddenId);
|
||||
if (import.meta.env.DEV) {
|
||||
const hiddenId = InvisibleCodec.encode(v7());
|
||||
console.time(message + hiddenId);
|
||||
try {
|
||||
const output = callback();
|
||||
return output;
|
||||
} finally {
|
||||
console.timeEnd(message + hiddenId);
|
||||
}
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
|
||||
export async function doTimedAsync<T>(message: string, callback: () => T): Promise<Awaited<T>> {
|
||||
const hiddenId = InvisibleCodec.encode(v7());
|
||||
console.time(message + hiddenId);
|
||||
try {
|
||||
const output = await callback();
|
||||
return output;
|
||||
} finally {
|
||||
console.timeEnd(message + hiddenId);
|
||||
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());
|
||||
console.time(message + hiddenId);
|
||||
try {
|
||||
const output = await callback();
|
||||
resolve(output);
|
||||
} catch (err) {
|
||||
console.error(`Error in timed operation "${message}":`, err);
|
||||
if (throwError) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(undefined as R);
|
||||
}
|
||||
} finally {
|
||||
console.timeEnd(message + hiddenId);
|
||||
}
|
||||
}) as R;
|
||||
} else {
|
||||
return callback() as R;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -838,7 +838,7 @@ function updateBulkDownloadProgress(notifEl, current, total, currentItem, progre
|
|||
const percent = progress.progress || 0;
|
||||
progressFill.style.width = `${percent}%`;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -849,7 +849,7 @@ function updateBulkDownloadProgress(notifEl, current, total, currentItem, progre
|
|||
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
||||
progressFill.style.width = `${percent}%`;
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { getCoverBlob, getTrackTitle } 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';
|
||||
const FLAC_BLOCK_TYPES = {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,6 @@ import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './tag
|
|||
import { doTimed, doTimedAsync } from './doTimed.ts';
|
||||
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) {
|
||||
const _tagLib = fetchTagLib().catch(console.error);
|
||||
const coverId = getTrackCoverId(track);
|
||||
|
|
|
|||
17
js/platform-detection.ts
Normal file
17
js/platform-detection.ts
Normal 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');
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { audioContextManager } from './audio-context.js';
|
||||
import { db } from './db.js';
|
||||
import Hls from 'hls.js';
|
||||
import { isIos } from './platform-detection.js';
|
||||
|
||||
export class Player {
|
||||
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
||||
|
|
@ -42,7 +43,7 @@ export class Player {
|
|||
this.isFallbackRetry = false;
|
||||
this.isFallbackInProgress = false;
|
||||
this.autoplayBlocked = false;
|
||||
this.isIOS = typeof window !== 'undefined' && window.__IS_IOS__ === true;
|
||||
this.isIOS = isIos;
|
||||
this.isPwa =
|
||||
typeof window !== 'undefined' &&
|
||||
(window.matchMedia?.('(display-mode: standalone)')?.matches || window.navigator?.standalone === true);
|
||||
|
|
|
|||
28
js/readableStreamIterator.ts
Normal file
28
js/readableStreamIterator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -830,6 +830,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
};
|
||||
const categoryOrder = ['Lossless', 'AAC', 'MP3', 'OGG'];
|
||||
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 bi = categoryOrder.indexOf(b.category);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
} catch (dbError) {
|
||||
console.log('Could not clear IndexedDB stores:', dbError);
|
||||
// Try to delete the entire database as fallback
|
||||
|
|
|
|||
10
styles.css
10
styles.css
|
|
@ -2737,6 +2737,15 @@ input[type='search']::-webkit-search-cancel-button {
|
|||
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 input[type='number'] {
|
||||
background-color: var(--input);
|
||||
|
|
@ -4936,7 +4945,6 @@ input:checked + .slider::before {
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.customize-shortcut-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export default defineConfig(({ mode }) => {
|
|||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [
|
||||
IS_NEUTRALINO && neutralino(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue