fix(purchases): restructure download paths to Artist/Album [FORMAT][Quality]/track

Embeds quality in album folder name instead of subfolder, so each
quality variant appears as a separate album in the local library.
Adds download error toast and fixes format labels to bracket notation.
Closes #116
This commit is contained in:
vicrodh committed 2026-03-05 11:12:39 -06:00
1 parent 3c50c1c14e
commit 43df8a40e4
7 files changed
+32 -33

No files matched your search

+16 -11
View File
@@ -10614,7 +10614,7 @@ fn v2_purchase_target_path(
ext: &str,
) -> PathBuf {
let artist_dir = crate::offline_cache::metadata::sanitize_filename(artist_name);
let album_dir = crate::offline_cache::metadata::sanitize_filename(album_title);
let album_clean = crate::offline_cache::metadata::sanitize_filename(album_title);
let title_clean = crate::offline_cache::metadata::sanitize_filename(track_title);
let file_name = if track_number > 0 {
@@ -10623,13 +10623,18 @@ fn v2_purchase_target_path(
format!("{}.{}", title_clean, ext)
};
let mut path = PathBuf::from(destination)
// Embed quality in album folder name: "Album [FLAC][24-bit,96kHz]"
let album_dir = if !quality_dir.is_empty() {
let quality_clean = crate::offline_cache::metadata::sanitize_filename(quality_dir);
format!("{} {}", album_clean, quality_clean)
} else {
album_clean
};
PathBuf::from(destination)
.join(artist_dir)
.join(album_dir);
if !quality_dir.is_empty() {
path = path.join(crate::offline_cache::metadata::sanitize_filename(quality_dir));
}
path.join(file_name)
.join(album_dir)
.join(file_name)
}
fn v2_apply_purchase_download_flags(
@@ -11000,7 +11005,7 @@ pub async fn v2_purchases_get_formats(
if album.hires && album.maximum_sampling_rate.unwrap_or(0.0) > 96.0 {
formats.push(V2PurchaseFormatOption {
id: 27,
label: "FLAC 24-bit / 192 kHz".to_string(),
label: "[FLAC][24-bit,192kHz]".to_string(),
bit_depth: Some(24),
sampling_rate: Some(192.0),
});
@@ -11009,7 +11014,7 @@ pub async fn v2_purchases_get_formats(
if album.hires {
formats.push(V2PurchaseFormatOption {
id: 7,
label: "FLAC 24-bit / 96 kHz".to_string(),
label: "[FLAC][24-bit,96kHz]".to_string(),
bit_depth: Some(24),
sampling_rate: Some(96.0),
});
@@ -11017,14 +11022,14 @@ pub async fn v2_purchases_get_formats(
formats.push(V2PurchaseFormatOption {
id: 6,
label: "FLAC 16-bit / 44.1 kHz".to_string(),
label: "[FLAC][16-bit,44.1kHz]".to_string(),
bit_depth: Some(16),
sampling_rate: Some(44.1),
});
formats.push(V2PurchaseFormatOption {
id: 5,
label: "MP3 320 kbps".to_string(),
label: "[MP3][320kbps]".to_string(),
bit_depth: None,
sampling_rate: None,
});
@@ -436,6 +436,7 @@
formatPickerAnchor = { top: rect.bottom + 4, right: window.innerWidth - rect.right };
} catch (err) {
console.error('Track download error:', err);
showToast($t('purchases.errors.downloadFailed'), 'error');
}
}
@@ -1865,7 +1866,7 @@
}
.track-row.downloaded {
opacity: 0.6;
opacity: 0.75;
}
/* Format picker popup */
+2 -1
View File
@@ -1719,7 +1719,8 @@
"regionNotice": "Die Verfügbarkeit von Käufen hängt von deinem Qobuz-Konto und deiner Region ab. Wenn es nicht verfügbar ist, kann das erwartet sein.",
"errors": {
"noAlbum": "Download nicht moeglich: Titel hat kein zugehoeriges Album.",
"noFormats": "Keine Download-Formate fuer diesen Titel verfuegbar."
"noFormats": "Keine Download-Formate fuer diesen Titel verfuegbar.",
"downloadFailed": "Download konnte nicht gestartet werden. Bitte erneut versuchen."
}
},
"playlistManager": {
+2 -1
View File
@@ -1900,7 +1900,8 @@
"regionNotice": "Purchases availability depends on your Qobuz account and region. If unavailable, this may be expected.",
"errors": {
"noAlbum": "Cannot download: track has no associated album.",
"noFormats": "No download formats available for this track."
"noFormats": "No download formats available for this track.",
"downloadFailed": "Failed to start download. Please try again."
}
},
"yourMixes": {
+2 -1
View File
@@ -1900,7 +1900,8 @@
"regionNotice": "La disponibilidad de Compras depende de tu cuenta y region de Qobuz. Si no aparece disponible, puede ser esperado.",
"errors": {
"noAlbum": "No se puede descargar: la pista no tiene album asociado.",
"noFormats": "No hay formatos de descarga disponibles para esta pista."
"noFormats": "No hay formatos de descarga disponibles para esta pista.",
"downloadFailed": "Error al iniciar la descarga. Intenta de nuevo."
}
},
"yourMixes": {
+2 -1
View File
@@ -1719,7 +1719,8 @@
"regionNotice": "La disponibilité des achats dépend de votre compte Qobuz et de votre région. Si ce n'est pas disponible, cela peut être normal.",
"errors": {
"noAlbum": "Telechargement impossible : la piste n'a pas d'album associe.",
"noFormats": "Aucun format de telechargement disponible pour cette piste."
"noFormats": "Aucun format de telechargement disponible pour cette piste.",
"downloadFailed": "Impossible de demarrer le telechargement. Veuillez reessayer."
}
},
"playlistManager": {
+6 -17
View File
@@ -81,25 +81,14 @@ export function startAlbumDownload(
/**
* Extract the album-level folder from a downloaded file path.
* Download structure: destination/artist/album/[qualityDir/]track.ext
* We want: destination/artist/album/
* Download structure: destination/artist/album [quality]/track.ext
* We want: destination/artist/album [quality]/
*/
function albumFolderFromFilePath(filePath: string, hasQualityDir: boolean): string {
// Remove filename
function albumFolderFromFilePath(filePath: string): string {
// Remove filename to get the album directory
let lastSep = filePath.lastIndexOf('/');
if (lastSep < 0) lastSep = filePath.lastIndexOf('\\');
let dir = lastSep >= 0 ? filePath.substring(0, lastSep) : filePath;
// If there's a quality subdirectory, go up one more level
if (hasQualityDir) {
let prevSep = dir.lastIndexOf('/');
if (prevSep < 0) prevSep = dir.lastIndexOf('\\');
if (prevSep >= 0) {
dir = dir.substring(0, prevSep);
}
}
return dir;
return lastSep >= 0 ? filePath.substring(0, lastSep) : filePath;
}
async function executeAlbumDownload(
@@ -144,7 +133,7 @@ async function executeAlbumDownload(
// so "Add to Library" adds only this album, not the entire root.
if (!albumFolderResolved) {
albumFolderResolved = true;
const albumFolder = albumFolderFromFilePath(filePath, qualityDir.length > 0);
const albumFolder = albumFolderFromFilePath(filePath);
updateAlbumState(albumId, (state) => ({
...state,
destination: albumFolder,