Skip to content

Commit fb04a7b

Browse files
amazon-music-library-downloader.user.js
1 parent 8f8ccd7 commit fb04a7b

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// ==UserScript==
2+
// @name Amazon Music Library Downloader
3+
// @namespace https://softwareengineerprogrammer.github.io/
4+
// @version 1.0
5+
// @description Automates downloading of all songs in Amazon Music library via quasi-infinite scroll
6+
// @match https://music.amazon.com/*
7+
// @grant none
8+
// ==/UserScript==
9+
10+
(function() {
11+
'use strict';
12+
13+
const STORAGE_KEY = 'amz_downloaded_keys';
14+
let isRunning = false;
15+
let downloadedKeys = new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'));
16+
let skippedKeys = new Set();
17+
18+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
19+
20+
/*
21+
* Injects a fixed control panel into the bottom right of the viewport
22+
* to manage the run state and clear persistent storage.
23+
*/
24+
function setupUI() {
25+
const container = document.createElement('div');
26+
container.style.position = 'fixed';
27+
container.style.bottom = '20px';
28+
container.style.right = '20px';
29+
container.style.backgroundColor = '#1a1a1a';
30+
container.style.color = '#ffffff';
31+
container.style.padding = '16px';
32+
container.style.borderRadius = '8px';
33+
container.style.zIndex = '999999';
34+
container.style.fontFamily = 'sans-serif';
35+
container.style.border = '1px solid #333';
36+
container.style.display = 'flex';
37+
container.style.flexDirection = 'column';
38+
container.style.gap = '12px';
39+
container.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)';
40+
41+
const statusText = document.createElement('div');
42+
statusText.id = 'amz-dl-status';
43+
statusText.innerText = `Downloaded: ${downloadedKeys.size} | Skipped: 0`;
44+
container.appendChild(statusText);
45+
46+
const btnContainer = document.createElement('div');
47+
btnContainer.style.display = 'flex';
48+
btnContainer.style.gap = '8px';
49+
50+
const toggleBtn = document.createElement('button');
51+
toggleBtn.innerText = 'Start';
52+
toggleBtn.style.cursor = 'pointer';
53+
toggleBtn.style.padding = '6px 12px';
54+
toggleBtn.style.backgroundColor = '#00a8e1';
55+
toggleBtn.style.color = '#fff';
56+
toggleBtn.style.border = 'none';
57+
toggleBtn.style.borderRadius = '4px';
58+
59+
const resetBtn = document.createElement('button');
60+
resetBtn.innerText = 'Reset Storage';
61+
resetBtn.style.cursor = 'pointer';
62+
resetBtn.style.padding = '6px 12px';
63+
resetBtn.style.backgroundColor = '#d9534f';
64+
resetBtn.style.color = '#fff';
65+
resetBtn.style.border = 'none';
66+
resetBtn.style.borderRadius = '4px';
67+
68+
btnContainer.appendChild(toggleBtn);
69+
btnContainer.appendChild(resetBtn);
70+
container.appendChild(btnContainer);
71+
document.body.appendChild(container);
72+
73+
toggleBtn.addEventListener('click', () => {
74+
isRunning = !isRunning;
75+
toggleBtn.innerText = isRunning ? 'Pause' : 'Start';
76+
toggleBtn.style.backgroundColor = isRunning ? '#f0ad4e' : '#00a8e1';
77+
78+
if (isRunning) {
79+
processLibrary();
80+
}
81+
});
82+
83+
resetBtn.addEventListener('click', () => {
84+
if (isRunning) {
85+
isRunning = false;
86+
toggleBtn.innerText = 'Start';
87+
toggleBtn.style.backgroundColor = '#00a8e1';
88+
}
89+
downloadedKeys.clear();
90+
skippedKeys.clear();
91+
localStorage.removeItem(STORAGE_KEY);
92+
updateStatusText();
93+
});
94+
}
95+
96+
function updateStatusText(suffix = '') {
97+
const el = document.getElementById('amz-dl-status');
98+
if (el) {
99+
el.innerText = `Downloaded: ${downloadedKeys.size} | Skipped: ${skippedKeys.size} ${suffix}`;
100+
}
101+
}
102+
103+
/*
104+
* Main execution loop. Amazon Music uses virtualized list rendering,
105+
* meaning DOM nodes are aggressively recycled as you scroll.
106+
* We continuously re-query the DOM to find the first un-processed node.
107+
*/
108+
async function processLibrary() {
109+
while (isRunning) {
110+
const rows = Array.from(document.querySelectorAll('music-image-row'));
111+
112+
if (rows.length === 0) {
113+
updateStatusText('(Waiting for rows...)');
114+
await sleep(2000);
115+
continue;
116+
}
117+
118+
const nextRow = rows.find(row => {
119+
const key = row.getAttribute('data-key');
120+
return key && !downloadedKeys.has(key) && !skippedKeys.has(key);
121+
});
122+
123+
if (nextRow) {
124+
const key = nextRow.getAttribute('data-key');
125+
126+
nextRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
127+
await sleep(600);
128+
129+
const contextBtn = nextRow.querySelector('music-button[slot="contextMenu"]');
130+
if (!contextBtn) {
131+
skippedKeys.add(key);
132+
updateStatusText();
133+
continue;
134+
}
135+
136+
contextBtn.click();
137+
await sleep(800);
138+
139+
const downloadOption = document.querySelector('#contextMenuOverlay music-list-item[primary-text="Download"]');
140+
141+
if (downloadOption) {
142+
downloadOption.click();
143+
downloadedKeys.add(key);
144+
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(downloadedKeys)));
145+
updateStatusText();
146+
147+
await sleep(1000);
148+
} else {
149+
document.body.click();
150+
await sleep(400);
151+
skippedKeys.add(key);
152+
updateStatusText('(Skipped track - No DL option)');
153+
}
154+
} else {
155+
/*
156+
* If we didn't find an unprocessed row, we've exhausted the current
157+
* virtualized DOM chunk. Scroll the last visible element into view
158+
* to trigger the next fetch.
159+
*/
160+
const lastRow = rows[rows.length - 1];
161+
lastRow.scrollIntoView({ behavior: 'smooth', block: 'start' });
162+
163+
await sleep(1500);
164+
165+
const loader = document.querySelector('music-shoveler music-icon[name="loader"]');
166+
if (!loader) {
167+
const newRows = Array.from(document.querySelectorAll('music-image-row'));
168+
const allProcessed = newRows.every(r => {
169+
const k = r.getAttribute('data-key');
170+
return downloadedKeys.has(k) || skippedKeys.has(k);
171+
});
172+
173+
if (allProcessed && newRows.length === rows.length) {
174+
isRunning = false;
175+
updateStatusText('(Finished or Stalled)');
176+
const toggleBtn = document.querySelector('button');
177+
if (toggleBtn) {
178+
toggleBtn.innerText = 'Start';
179+
toggleBtn.style.backgroundColor = '#00a8e1';
180+
}
181+
break;
182+
}
183+
} else {
184+
updateStatusText('(Loading more...)');
185+
await sleep(1500);
186+
}
187+
}
188+
}
189+
}
190+
191+
window.addEventListener('load', () => {
192+
setTimeout(setupUI, 2000);
193+
});
194+
195+
})();

0 commit comments

Comments
 (0)