feat(downloads): add FLAC - Max Compression option and refactor transcoding logic
This commit is contained in:
parent
2db782d74f
commit
7448ddce1e
6 changed files with 282 additions and 208 deletions
|
|
@ -5117,11 +5117,7 @@
|
||||||
<span class="label">Lossless Container</span>
|
<span class="label">Lossless Container</span>
|
||||||
<span class="description">Container format for lossless downloads</span>
|
<span class="description">Container format for lossless downloads</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="lossless-container-setting">
|
<select id="lossless-container-setting"></select>
|
||||||
<option value="flac">FLAC</option>
|
|
||||||
<option value="alac">Apple Lossless</option>
|
|
||||||
<option value="nochange">Don't change</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
|
|
||||||
41
js/api.js
41
js/api.js
|
|
@ -12,9 +12,15 @@ import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
||||||
import { DashDownloader } from './dash-downloader.js';
|
import { DashDownloader } from './dash-downloader.js';
|
||||||
import { HlsDownloader } from './hls-downloader.js';
|
import { HlsDownloader } from './hls-downloader.js';
|
||||||
import { MP3EncodingError } from './mp3-encoder.js';
|
import { MP3EncodingError } from './mp3-encoder.js';
|
||||||
import { ffmpeg, loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
import { loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
||||||
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||||
import { isCustomFormat, getCustomFormat, transcodeWithCustomFormat } from './customFormats.ts';
|
import {
|
||||||
|
isCustomFormat,
|
||||||
|
getCustomFormat,
|
||||||
|
transcodeWithCustomFormat,
|
||||||
|
getContainerFormat,
|
||||||
|
transcodeWithContainerFormat,
|
||||||
|
} from './ffmpegFormats.ts';
|
||||||
|
|
||||||
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
|
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
|
||||||
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
|
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
|
||||||
|
|
@ -1437,33 +1443,18 @@ export class LosslessAPI {
|
||||||
|
|
||||||
if (quality.endsWith('LOSSLESS')) {
|
if (quality.endsWith('LOSSLESS')) {
|
||||||
try {
|
try {
|
||||||
switch (losslessContainerSettings.getContainer()) {
|
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
||||||
case 'flac':
|
if (containerFmt) {
|
||||||
if ((await getExtensionFromBlob(blob)) != 'flac') {
|
if (await containerFmt.needsTranscode(blob)) {
|
||||||
blob = await ffmpeg(
|
blob = await transcodeWithContainerFormat(
|
||||||
blob,
|
|
||||||
{ args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] },
|
|
||||||
'output.flac',
|
|
||||||
'audio/flac',
|
|
||||||
onProgress,
|
|
||||||
options.signal
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
blob = await rebuildFlacWithoutMetadata(blob);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'alac':
|
|
||||||
blob = await ffmpeg(
|
|
||||||
blob,
|
blob,
|
||||||
{ args: ['-c:a', 'alac'] },
|
containerFmt,
|
||||||
'output.m4a',
|
|
||||||
'audio/mp4',
|
|
||||||
onProgress,
|
onProgress,
|
||||||
options.signal
|
options.signal
|
||||||
);
|
);
|
||||||
break;
|
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
||||||
default:
|
blob = await rebuildFlacWithoutMetadata(blob);
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') {
|
if (error?.name === 'AbortError') {
|
||||||
|
|
|
||||||
|
|
@ -1,148 +1,13 @@
|
||||||
import { ffmpeg } from './ffmpeg';
|
// Re-exports for backwards compatibility – canonical source is ffmpegFormats.ts
|
||||||
|
export {
|
||||||
export interface ProgressEvent {
|
type ProgressEvent,
|
||||||
stage?: string;
|
type CustomFormat,
|
||||||
message?: string;
|
type ContainerFormat,
|
||||||
progress?: number;
|
customFormats,
|
||||||
receivedBytes?: number;
|
containerFormats,
|
||||||
totalBytes?: number;
|
isCustomFormat,
|
||||||
}
|
getCustomFormat,
|
||||||
|
getContainerFormat,
|
||||||
export interface CustomFormat {
|
transcodeWithCustomFormat,
|
||||||
/** Human-readable label shown in the UI */
|
transcodeWithContainerFormat,
|
||||||
displayName: string;
|
} from './ffmpegFormats';
|
||||||
/** Internal identifier, must start with `FFMPEG_` */
|
|
||||||
internalName: string;
|
|
||||||
/** Arguments passed to ffmpeg (excluding input/output file args) */
|
|
||||||
ffmpegArgs: string[];
|
|
||||||
/** Output filename used when calling ffmpeg */
|
|
||||||
outputFilename: string;
|
|
||||||
/** MIME type of the encoded output */
|
|
||||||
outputMime: string;
|
|
||||||
/** File extension of the encoded output */
|
|
||||||
extension: string;
|
|
||||||
/** Category label used for grouping in the UI (e.g. 'MP3', 'OGG', 'AAC') */
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customFormats: CustomFormat[] = [
|
|
||||||
{
|
|
||||||
displayName: 'MP3 320kbps',
|
|
||||||
internalName: 'FFMPEG_MP3_320',
|
|
||||||
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '320k', '-ar', '44100'],
|
|
||||||
outputFilename: 'output.mp3',
|
|
||||||
outputMime: 'audio/mpeg',
|
|
||||||
extension: 'mp3',
|
|
||||||
category: 'MP3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'MP3 256kbps',
|
|
||||||
internalName: 'FFMPEG_MP3_256',
|
|
||||||
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '256k', '-ar', '44100'],
|
|
||||||
outputFilename: 'output.mp3',
|
|
||||||
outputMime: 'audio/mpeg',
|
|
||||||
extension: 'mp3',
|
|
||||||
category: 'MP3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'MP3 128kbps',
|
|
||||||
internalName: 'FFMPEG_MP3_128',
|
|
||||||
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '128k', '-ar', '44100'],
|
|
||||||
outputFilename: 'output.mp3',
|
|
||||||
outputMime: 'audio/mpeg',
|
|
||||||
extension: 'mp3',
|
|
||||||
category: 'MP3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'OGG 320kbps',
|
|
||||||
internalName: 'FFMPEG_OGG_320',
|
|
||||||
ffmpegArgs: [
|
|
||||||
'-map_metadata',
|
|
||||||
'-1',
|
|
||||||
'-c:a',
|
|
||||||
'libvorbis',
|
|
||||||
'-b:a',
|
|
||||||
'320k',
|
|
||||||
'-minrate',
|
|
||||||
'320k',
|
|
||||||
'-maxrate',
|
|
||||||
'320k',
|
|
||||||
],
|
|
||||||
outputFilename: 'output.ogg',
|
|
||||||
outputMime: 'audio/ogg',
|
|
||||||
extension: 'ogg',
|
|
||||||
category: 'OGG',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'OGG 256kbps',
|
|
||||||
internalName: 'FFMPEG_OGG_256',
|
|
||||||
ffmpegArgs: [
|
|
||||||
'-map_metadata',
|
|
||||||
'-1',
|
|
||||||
'-c:a',
|
|
||||||
'libvorbis',
|
|
||||||
'-b:a',
|
|
||||||
'256k',
|
|
||||||
'-minrate',
|
|
||||||
'256k',
|
|
||||||
'-maxrate',
|
|
||||||
'256k',
|
|
||||||
],
|
|
||||||
outputFilename: 'output.ogg',
|
|
||||||
outputMime: 'audio/ogg',
|
|
||||||
extension: 'ogg',
|
|
||||||
category: 'OGG',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'OGG 128kbps',
|
|
||||||
internalName: 'FFMPEG_OGG_128',
|
|
||||||
ffmpegArgs: [
|
|
||||||
'-map_metadata',
|
|
||||||
'-1',
|
|
||||||
'-c:a',
|
|
||||||
'libvorbis',
|
|
||||||
'-b:a',
|
|
||||||
'128k',
|
|
||||||
'-minrate',
|
|
||||||
'128k',
|
|
||||||
'-maxrate',
|
|
||||||
'128k',
|
|
||||||
],
|
|
||||||
outputFilename: 'output.ogg',
|
|
||||||
outputMime: 'audio/ogg',
|
|
||||||
extension: 'ogg',
|
|
||||||
category: 'OGG',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'AAC 256kbps',
|
|
||||||
internalName: 'FFMPEG_AAC_256',
|
|
||||||
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'aac', '-b:a', '256k'],
|
|
||||||
outputFilename: 'output.m4a',
|
|
||||||
outputMime: 'audio/mp4',
|
|
||||||
extension: 'm4a',
|
|
||||||
category: 'AAC',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Returns true if the quality string identifies a known custom ffmpeg-transcoded format */
|
|
||||||
export function isCustomFormat(quality: string): boolean {
|
|
||||||
return getCustomFormat(quality) !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Looks up a custom format by its internal name, or returns undefined */
|
|
||||||
export function getCustomFormat(internalName: string): CustomFormat | undefined {
|
|
||||||
return customFormats.find((f) => f.internalName === internalName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transcodes an audio blob using the specified custom format via ffmpeg.
|
|
||||||
* Throws if ffmpeg fails during transcoding.
|
|
||||||
*/
|
|
||||||
export async function transcodeWithCustomFormat(
|
|
||||||
audioBlob: Blob,
|
|
||||||
format: CustomFormat,
|
|
||||||
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
|
||||||
signal: AbortSignal | null = null
|
|
||||||
): Promise<Blob> {
|
|
||||||
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,14 @@ import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
||||||
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||||
import { DashDownloader } from './dash-downloader.js';
|
import { DashDownloader } from './dash-downloader.js';
|
||||||
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
||||||
import { ffmpeg, loadFfmpeg } from './ffmpeg.js';
|
import { loadFfmpeg } from './ffmpeg.js';
|
||||||
import { isCustomFormat, getCustomFormat, transcodeWithCustomFormat } from './customFormats.ts';
|
import {
|
||||||
|
isCustomFormat,
|
||||||
|
getCustomFormat,
|
||||||
|
transcodeWithCustomFormat,
|
||||||
|
getContainerFormat,
|
||||||
|
transcodeWithContainerFormat,
|
||||||
|
} from './ffmpegFormats.ts';
|
||||||
|
|
||||||
const downloadTasks = new Map();
|
const downloadTasks = new Map();
|
||||||
const bulkDownloadTasks = new Map();
|
const bulkDownloadTasks = new Map();
|
||||||
|
|
@ -455,33 +461,13 @@ async function downloadTrackBlob(
|
||||||
|
|
||||||
if (quality.endsWith('LOSSLESS')) {
|
if (quality.endsWith('LOSSLESS')) {
|
||||||
try {
|
try {
|
||||||
switch (losslessContainerSettings.getContainer()) {
|
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
||||||
case 'flac':
|
if (containerFmt) {
|
||||||
if ((await getExtensionFromBlob(blob)) != 'flac') {
|
if (await containerFmt.needsTranscode(blob)) {
|
||||||
blob = await ffmpeg(
|
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
||||||
blob,
|
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
||||||
{ args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] },
|
blob = await rebuildFlacWithoutMetadata(blob);
|
||||||
'output.flac',
|
}
|
||||||
'audio/flac',
|
|
||||||
onProgress,
|
|
||||||
signal
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
blob = await rebuildFlacWithoutMetadata(blob);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'alac':
|
|
||||||
blob = await ffmpeg(
|
|
||||||
blob,
|
|
||||||
{ args: ['-c:a', 'alac'] },
|
|
||||||
'output.m4a',
|
|
||||||
'audio/mp4',
|
|
||||||
onProgress,
|
|
||||||
signal
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') {
|
if (error?.name === 'AbortError') {
|
||||||
|
|
|
||||||
229
js/ffmpegFormats.ts
Normal file
229
js/ffmpegFormats.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
import { ffmpeg } from './ffmpeg';
|
||||||
|
import { getExtensionFromBlob } from './utils';
|
||||||
|
|
||||||
|
export interface ProgressEvent {
|
||||||
|
stage?: string;
|
||||||
|
message?: string;
|
||||||
|
progress?: number;
|
||||||
|
receivedBytes?: number;
|
||||||
|
totalBytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomFormat {
|
||||||
|
/** Human-readable label shown in the UI */
|
||||||
|
displayName: string;
|
||||||
|
/** Internal identifier, must start with `FFMPEG_` */
|
||||||
|
internalName: string;
|
||||||
|
/** Arguments passed to ffmpeg (excluding input/output file args) */
|
||||||
|
ffmpegArgs: string[];
|
||||||
|
/** Output filename used when calling ffmpeg */
|
||||||
|
outputFilename: string;
|
||||||
|
/** MIME type of the encoded output */
|
||||||
|
outputMime: string;
|
||||||
|
/** File extension of the encoded output */
|
||||||
|
extension: string;
|
||||||
|
/** Category label used for grouping in the UI (e.g. 'MP3', 'OGG', 'AAC') */
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container format definition for lossless re-muxing/re-encoding.
|
||||||
|
* Extends CustomFormat with a callback that decides whether ffmpeg needs to run
|
||||||
|
* at all (e.g. FLAC can skip if the source is already FLAC).
|
||||||
|
*/
|
||||||
|
export interface ContainerFormat extends Omit<CustomFormat, 'category'> {
|
||||||
|
/**
|
||||||
|
* Returns true when the source blob must be passed through ffmpeg to produce
|
||||||
|
* the desired container. Return false to skip the ffmpeg step (the caller
|
||||||
|
* may still apply a lightweight metadata-strip pass instead).
|
||||||
|
*/
|
||||||
|
needsTranscode: (blob: Blob) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customFormats: CustomFormat[] = [
|
||||||
|
{
|
||||||
|
displayName: 'MP3 320kbps',
|
||||||
|
internalName: 'FFMPEG_MP3_320',
|
||||||
|
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '320k', '-ar', '44100'],
|
||||||
|
outputFilename: 'output.mp3',
|
||||||
|
outputMime: 'audio/mpeg',
|
||||||
|
extension: 'mp3',
|
||||||
|
category: 'MP3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'MP3 256kbps',
|
||||||
|
internalName: 'FFMPEG_MP3_256',
|
||||||
|
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '256k', '-ar', '44100'],
|
||||||
|
outputFilename: 'output.mp3',
|
||||||
|
outputMime: 'audio/mpeg',
|
||||||
|
extension: 'mp3',
|
||||||
|
category: 'MP3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'MP3 128kbps',
|
||||||
|
internalName: 'FFMPEG_MP3_128',
|
||||||
|
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '128k', '-ar', '44100'],
|
||||||
|
outputFilename: 'output.mp3',
|
||||||
|
outputMime: 'audio/mpeg',
|
||||||
|
extension: 'mp3',
|
||||||
|
category: 'MP3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'OGG 320kbps',
|
||||||
|
internalName: 'FFMPEG_OGG_320',
|
||||||
|
ffmpegArgs: [
|
||||||
|
'-map_metadata',
|
||||||
|
'-1',
|
||||||
|
'-c:a',
|
||||||
|
'libvorbis',
|
||||||
|
'-b:a',
|
||||||
|
'320k',
|
||||||
|
'-minrate',
|
||||||
|
'320k',
|
||||||
|
'-maxrate',
|
||||||
|
'320k',
|
||||||
|
],
|
||||||
|
outputFilename: 'output.ogg',
|
||||||
|
outputMime: 'audio/ogg',
|
||||||
|
extension: 'ogg',
|
||||||
|
category: 'OGG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'OGG 256kbps',
|
||||||
|
internalName: 'FFMPEG_OGG_256',
|
||||||
|
ffmpegArgs: [
|
||||||
|
'-map_metadata',
|
||||||
|
'-1',
|
||||||
|
'-c:a',
|
||||||
|
'libvorbis',
|
||||||
|
'-b:a',
|
||||||
|
'256k',
|
||||||
|
'-minrate',
|
||||||
|
'256k',
|
||||||
|
'-maxrate',
|
||||||
|
'256k',
|
||||||
|
],
|
||||||
|
outputFilename: 'output.ogg',
|
||||||
|
outputMime: 'audio/ogg',
|
||||||
|
extension: 'ogg',
|
||||||
|
category: 'OGG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'OGG 128kbps',
|
||||||
|
internalName: 'FFMPEG_OGG_128',
|
||||||
|
ffmpegArgs: [
|
||||||
|
'-map_metadata',
|
||||||
|
'-1',
|
||||||
|
'-c:a',
|
||||||
|
'libvorbis',
|
||||||
|
'-b:a',
|
||||||
|
'128k',
|
||||||
|
'-minrate',
|
||||||
|
'128k',
|
||||||
|
'-maxrate',
|
||||||
|
'128k',
|
||||||
|
],
|
||||||
|
outputFilename: 'output.ogg',
|
||||||
|
outputMime: 'audio/ogg',
|
||||||
|
extension: 'ogg',
|
||||||
|
category: 'OGG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'AAC 256kbps',
|
||||||
|
internalName: 'FFMPEG_AAC_256',
|
||||||
|
ffmpegArgs: ['-map_metadata', '-1', '-c:a', 'aac', '-b:a', '256k'],
|
||||||
|
outputFilename: 'output.m4a',
|
||||||
|
outputMime: 'audio/mp4',
|
||||||
|
extension: 'm4a',
|
||||||
|
category: 'AAC',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container format definitions for lossless re-muxing. Each entry describes
|
||||||
|
* the ffmpeg arguments needed to produce that container and provides a
|
||||||
|
* `needsTranscode` predicate so callers can skip the ffmpeg step when the
|
||||||
|
* source is already in the correct container.
|
||||||
|
*/
|
||||||
|
export const containerFormats: ContainerFormat[] = [
|
||||||
|
{
|
||||||
|
displayName: 'FLAC',
|
||||||
|
internalName: 'flac',
|
||||||
|
ffmpegArgs: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac', '-compression_level', '12'],
|
||||||
|
outputFilename: 'output.flac',
|
||||||
|
outputMime: 'audio/flac',
|
||||||
|
extension: 'flac',
|
||||||
|
// Only transcode when the source is NOT already a FLAC file.
|
||||||
|
needsTranscode: async (blob) => (await getExtensionFromBlob(blob)) !== 'flac',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'FLAC - Max Compression',
|
||||||
|
internalName: 'flac_max',
|
||||||
|
// `-compression_level 12` is the highest FLAC compression level; audio
|
||||||
|
// data is bit-identical to the source — only the compressed size changes.
|
||||||
|
ffmpegArgs: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac', '-compression_level', '12'],
|
||||||
|
outputFilename: 'output.flac',
|
||||||
|
outputMime: 'audio/flac',
|
||||||
|
extension: 'flac',
|
||||||
|
needsTranscode: async () => true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Apple Lossless',
|
||||||
|
internalName: 'alac',
|
||||||
|
ffmpegArgs: ['-c:a', 'alac'],
|
||||||
|
outputFilename: 'output.m4a',
|
||||||
|
outputMime: 'audio/mp4',
|
||||||
|
extension: 'm4a',
|
||||||
|
needsTranscode: async () => true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "Don't change",
|
||||||
|
internalName: 'nochange',
|
||||||
|
ffmpegArgs: [],
|
||||||
|
outputFilename: '',
|
||||||
|
outputMime: '',
|
||||||
|
extension: '',
|
||||||
|
needsTranscode: async () => false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Returns true if the quality string identifies a known custom ffmpeg-transcoded format */
|
||||||
|
export function isCustomFormat(quality: string): boolean {
|
||||||
|
return getCustomFormat(quality) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Looks up a custom format by its internal name, or returns undefined */
|
||||||
|
export function getCustomFormat(internalName: string): CustomFormat | undefined {
|
||||||
|
return customFormats.find((f) => f.internalName === internalName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Looks up a container format by its internal name, or returns undefined */
|
||||||
|
export function getContainerFormat(internalName: string): ContainerFormat | undefined {
|
||||||
|
return containerFormats.find((f) => f.internalName === internalName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transcodes an audio blob using the specified custom format via ffmpeg.
|
||||||
|
* Throws if ffmpeg fails during transcoding.
|
||||||
|
*/
|
||||||
|
export async function transcodeWithCustomFormat(
|
||||||
|
audioBlob: Blob,
|
||||||
|
format: CustomFormat,
|
||||||
|
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
||||||
|
signal: AbortSignal | null = null
|
||||||
|
): Promise<Blob> {
|
||||||
|
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-muxes / re-encodes an audio blob into the specified container format via ffmpeg.
|
||||||
|
* Throws if ffmpeg fails during transcoding.
|
||||||
|
*/
|
||||||
|
export async function transcodeWithContainerFormat(
|
||||||
|
audioBlob: Blob,
|
||||||
|
format: ContainerFormat,
|
||||||
|
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
||||||
|
signal: AbortSignal | null = null
|
||||||
|
): Promise<Blob> {
|
||||||
|
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
||||||
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ import { db } from './db.js';
|
||||||
import { authManager } from './accounts/auth.js';
|
import { authManager } from './accounts/auth.js';
|
||||||
import { syncManager } from './accounts/pocketbase.js';
|
import { syncManager } from './accounts/pocketbase.js';
|
||||||
import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js';
|
import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js';
|
||||||
import { customFormats } from './customFormats.ts';
|
import { containerFormats, customFormats } from './ffmpegFormats.ts';
|
||||||
|
|
||||||
export function initializeSettings(scrobbler, player, api, ui) {
|
export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
// Restore last active settings tab
|
// Restore last active settings tab
|
||||||
|
|
@ -867,6 +867,13 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
|
|
||||||
const losslessContainerSetting = document.getElementById('lossless-container-setting');
|
const losslessContainerSetting = document.getElementById('lossless-container-setting');
|
||||||
if (losslessContainerSetting) {
|
if (losslessContainerSetting) {
|
||||||
|
for (const { internalName, displayName } of containerFormats) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = internalName;
|
||||||
|
option.textContent = displayName;
|
||||||
|
losslessContainerSetting.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
losslessContainerSetting.value = losslessContainerSettings.getContainer();
|
losslessContainerSetting.value = losslessContainerSettings.getContainer();
|
||||||
|
|
||||||
losslessContainerSetting.addEventListener('change', (e) => {
|
losslessContainerSetting.addEventListener('change', (e) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue