Merge pull request #363 from DanTheMan827/video-metadata
Add metadata support for video downloads
This commit is contained in:
commit
9071670ad9
3 changed files with 100 additions and 80 deletions
67
js/HiFi.ts
67
js/HiFi.ts
|
|
@ -20,8 +20,9 @@ export class TidalResponse extends Response {
|
|||
|
||||
export class HiFiClient {
|
||||
private static token: string | null;
|
||||
private countryCode: string;
|
||||
private static appTokenExpiry = 0;
|
||||
private static tokenPromise: Promise<string> | null = null;
|
||||
private countryCode: string;
|
||||
private static albumTracksMax = 20;
|
||||
private static albumTracksActive = 0;
|
||||
private static albumTracksQueue: Array<() => void> = [];
|
||||
|
|
@ -35,7 +36,7 @@ export class HiFiClient {
|
|||
return u.toString();
|
||||
}
|
||||
|
||||
private encodeBasic(id: string, secret: string) {
|
||||
private static encodeBasic(id: string, secret: string) {
|
||||
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
|
||||
return window.btoa(`${id}:${secret}`);
|
||||
}
|
||||
|
|
@ -43,35 +44,47 @@ export class HiFiClient {
|
|||
return Buffer.from(`${id}:${secret}`).toString('base64');
|
||||
}
|
||||
|
||||
private async fetchAppToken(signal: AbortSignal = new AbortController().signal): Promise<string> {
|
||||
private static async fetchAppToken(signal: AbortSignal = new AbortController().signal) {
|
||||
const now = Date.now();
|
||||
HiFiClient.token ??= localStorage.getItem('hifi_token') || null;
|
||||
HiFiClient.appTokenExpiry = Number(localStorage.getItem('hifi_token_expiry') || '0');
|
||||
|
||||
if (HiFiClient.token && now < HiFiClient.appTokenExpiry) return HiFiClient.token;
|
||||
|
||||
const res = await fetch('https://auth.tidal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
authorization: `Basic ${this.encodeBasic(CLIENT_ID, CLIENT_SECRET)}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
return await (HiFiClient.tokenPromise ??= (async () => {
|
||||
try {
|
||||
const res = await fetch('https://auth.tidal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
authorization: `Basic ${this.encodeBasic(CLIENT_ID, CLIENT_SECRET)}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '');
|
||||
throw new Error(`Failed to obtain app token: ${res.status} ${txt}`);
|
||||
}
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '');
|
||||
throw new Error(`Failed to obtain app token: ${res.status} ${txt}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const token = json.access_token;
|
||||
const expires_in = json.expires_in ?? 3600;
|
||||
HiFiClient.token = token;
|
||||
HiFiClient.appTokenExpiry = Date.now() + expires_in * 1000 - 60_000;
|
||||
return token;
|
||||
const json = await res.json();
|
||||
const token = json.access_token;
|
||||
const expires_in = json.expires_in ?? 3600;
|
||||
HiFiClient.token = token;
|
||||
HiFiClient.appTokenExpiry = Date.now() + expires_in * 1000 - 60_000;
|
||||
localStorage.setItem('hifi_token', token);
|
||||
localStorage.setItem('hifi_token_expiry', HiFiClient.appTokenExpiry.toString());
|
||||
|
||||
return token;
|
||||
} finally {
|
||||
HiFiClient.tokenPromise = null;
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
constructor(countryCode = 'US') {
|
||||
|
|
@ -81,7 +94,7 @@ export class HiFiClient {
|
|||
private async fetchJson(url: string, params?: Params, signal: AbortSignal = new AbortController().signal) {
|
||||
const final = HiFiClient.buildUrl(url, params);
|
||||
const res = await fetch(final, {
|
||||
headers: { authorization: `Bearer ${await this.fetchAppToken(signal)}` },
|
||||
headers: { authorization: `Bearer ${await HiFiClient.fetchAppToken(signal)}` },
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
|
|||
107
js/api.js
107
js/api.js
|
|
@ -15,7 +15,7 @@ import { APICache } from './cache.js';
|
|||
import { DashDownloader } from './dash-downloader.ts';
|
||||
import { HlsDownloader } from './hls-downloader.js';
|
||||
import { MP3EncodingError } from './mp3-encoder.js';
|
||||
import { loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
||||
import { loadFfmpeg, FfmpegError, ffmpeg } from './ffmpeg.js';
|
||||
import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts';
|
||||
import { isCustomFormat } from './ffmpegFormats.ts';
|
||||
import { DownloadProgress } from './progressEvents.js';
|
||||
|
|
@ -56,11 +56,14 @@ export class LosslessAPI {
|
|||
|
||||
async fetchWithRetry(relativePath, options = {}) {
|
||||
const type = options.type || 'api';
|
||||
const instanceRoutes = ['/track', '/album/similar', '/artist/similar', '/video'];
|
||||
const instanceRoutes = ['/track', '/album/similar', '/artist/similar', '/video', '/recommendations'];
|
||||
|
||||
if (window.allTidal == true || !instanceRoutes.some((route) => relativePath.startsWith(route))) {
|
||||
try {
|
||||
console.log(relativePath);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(relativePath);
|
||||
}
|
||||
|
||||
return await client.queryResponse(relativePath);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
|
|
@ -1504,59 +1507,63 @@ export class LosslessAPI {
|
|||
|
||||
if (!isVideo) {
|
||||
blob = await applyAudioPostProcessing(blob, quality, onProgress, options.signal);
|
||||
}
|
||||
|
||||
// Add metadata if track information is provided
|
||||
if (track) {
|
||||
onProgress?.({
|
||||
stage: 'processing',
|
||||
message: 'Adding metadata...',
|
||||
});
|
||||
// Add metadata if track information is provided
|
||||
if (track) {
|
||||
onProgress?.({
|
||||
stage: 'processing',
|
||||
message: 'Adding metadata...',
|
||||
});
|
||||
|
||||
const enrichedTrack = { ...track };
|
||||
if (lookup.info) {
|
||||
enrichedTrack.replayGain = {
|
||||
trackReplayGain: lookup.info.trackReplayGain,
|
||||
trackPeakAmplitude: lookup.info.trackPeakAmplitude,
|
||||
albumReplayGain: lookup.info.albumReplayGain,
|
||||
albumPeakAmplitude: lookup.info.albumPeakAmplitude,
|
||||
};
|
||||
}
|
||||
const enrichedTrack = { ...track };
|
||||
if (lookup.info) {
|
||||
enrichedTrack.replayGain = {
|
||||
trackReplayGain: lookup.info.trackReplayGain,
|
||||
trackPeakAmplitude: lookup.info.trackPeakAmplitude,
|
||||
albumReplayGain: lookup.info.albumReplayGain,
|
||||
albumPeakAmplitude: lookup.info.albumPeakAmplitude,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
track.album?.id &&
|
||||
(track.album?.totalDiscs == null || track.album?.numberOfTracksOnDisc == null)
|
||||
) {
|
||||
try {
|
||||
const albumData = await this.getAlbum(track.album.id);
|
||||
if (albumData.tracks?.length > 0) {
|
||||
const discTrackCounts = new Map();
|
||||
let maxDiscNumber = 0;
|
||||
for (const t of albumData.tracks) {
|
||||
const dn = getTrackDiscNumber(t);
|
||||
discTrackCounts.set(dn, (discTrackCounts.get(dn) || 0) + 1);
|
||||
if (dn > maxDiscNumber) maxDiscNumber = dn;
|
||||
}
|
||||
const totalDiscs = maxDiscNumber || 1;
|
||||
const discNumber = getTrackDiscNumber(track);
|
||||
enrichedTrack.album = {
|
||||
...(enrichedTrack.album || {}),
|
||||
totalDiscs: track.album?.totalDiscs ?? totalDiscs,
|
||||
numberOfTracksOnDisc:
|
||||
track.album?.numberOfTracksOnDisc ?? discTrackCounts.get(discNumber),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch album for disc info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(new DownloadProgress('Adding metadata'));
|
||||
if (track.album?.id && (track.album?.totalDiscs == null || track.album?.numberOfTracksOnDisc == null)) {
|
||||
try {
|
||||
blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const albumData = await this.getAlbum(track.album.id);
|
||||
if (albumData.tracks?.length > 0) {
|
||||
const discTrackCounts = new Map();
|
||||
let maxDiscNumber = 0;
|
||||
for (const t of albumData.tracks) {
|
||||
const dn = getTrackDiscNumber(t);
|
||||
discTrackCounts.set(dn, (discTrackCounts.get(dn) || 0) + 1);
|
||||
if (dn > maxDiscNumber) maxDiscNumber = dn;
|
||||
}
|
||||
const totalDiscs = maxDiscNumber || 1;
|
||||
const discNumber = getTrackDiscNumber(track);
|
||||
enrichedTrack.album = {
|
||||
...(enrichedTrack.album || {}),
|
||||
totalDiscs: track.album?.totalDiscs ?? totalDiscs,
|
||||
numberOfTracksOnDisc:
|
||||
track.album?.numberOfTracksOnDisc ?? discTrackCounts.get(discNumber),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch album for disc info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(new DownloadProgress('Adding metadata'));
|
||||
try {
|
||||
if (isVideo) {
|
||||
blob = new File(
|
||||
[await ffmpeg(blob, ['-c', 'copy'], 'output.mp4', 'video/mp4', onProgress, options.signal)],
|
||||
'output.mp4',
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
}
|
||||
blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.triggerDownload ?? true) {
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
|||
try {
|
||||
data.title = getTrackTitle(track);
|
||||
data.artist = getFullArtistString(track);
|
||||
data.albumTitle = track.album.title;
|
||||
data.albumTitle = track.album?.title;
|
||||
data.albumArtist = track.album?.artist?.name || track.artist?.name;
|
||||
data.trackNumber = track.trackNumber;
|
||||
data.discNumber = track.volumeNumber ?? track.discNumber;
|
||||
data.totalTracks = track.album.numberOfTracksOnDisc ?? track.album.numberOfTracks;
|
||||
data.totalDiscs = track.album.totalDiscs;
|
||||
data.totalTracks = track.album?.numberOfTracksOnDisc ?? track.album?.numberOfTracks;
|
||||
data.totalDiscs = track.album?.totalDiscs;
|
||||
data.copyright = track.copyright;
|
||||
data.isrc = track.isrc;
|
||||
data.explicit = Boolean(track.explicit);
|
||||
|
|
|
|||
Loading…
Reference in a new issue