Skip to content

Commit 8083f04

Browse files
authored
Merge pull request #209 from sabjorn/feature/208-make-downloads-parallel
Feature/208 make downloads parallel
2 parents 87aa579 + 44f2536 commit 8083f04

File tree

3 files changed

+138
-41
lines changed

3 files changed

+138
-41
lines changed

css/style.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,29 @@ input.currency-input::-webkit-outer-spin-button {
274274
padding-bottom: .5em;
275275
text-align: center;
276276
}
277+
278+
button.bes-downloadall:hover {
279+
text-decoration: none;
280+
}
281+
282+
button.bes-downloadall[disabled] {
283+
cursor: default;
284+
opacity: 0.30;
285+
color: #000000;
286+
}
287+
288+
@keyframes loadingDots {
289+
0% { content: ""; }
290+
25% { content: "."; }
291+
50% { content: ".."; }
292+
75% { content: "..."; }
293+
100% { content: ""; }
294+
}
295+
296+
button.bes-downloadall[disabled]::after {
297+
content: "";
298+
display: inline-block;
299+
animation: loadingDots 1.5s infinite;
300+
width: 1.1em;
301+
text-align: left;
302+
}

src/download_helper.js

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@ import Logger from "./logger";
22

33
import { downloadFile, dateString } from "./utilities";
44

5-
const preamble = `#!/usr/bin/env bash
6-
# Generated by Bandcamp Enhancement Suite (https://github.com/sabjorn/BandcampEnhancementSuite)
7-
#
8-
# The following can be used to batch download your recent purchase.
9-
#
10-
# Usage (Mac/Linux):
11-
# 1) open Terminal
12-
# 2) move to desired download directory (e.g. \`cd ~/Downloads/bandcamp\`)
13-
# 3) paste the text of this file into Terminal\n\n`;
14-
155
export default class DownloadHelper {
166
constructor() {
177
this.log = new Logger();
@@ -52,7 +42,7 @@ export default class DownloadHelper {
5242
this.button = document.createElement("button");
5343
this.button.title =
5444
"Generates a file for automating downloads using 'cURL'";
55-
this.button.className = "downloadall";
45+
this.button.className = "bes-downloadall";
5646
this.button.disabled = true;
5747
this.button.textContent = "preparing download";
5848

@@ -69,7 +59,8 @@ export default class DownloadHelper {
6959
const date = DownloadHelper.dateString();
7060
const downloadList = DownloadHelper.generateDownloadList();
7161
const preamble = DownloadHelper.getDownloadPreamble();
72-
const downloadDocument = preamble + downloadList;
62+
const postamble = DownloadHelper.getDownloadPostamble();
63+
const downloadDocument = preamble + downloadList + postamble;
7364

7465
DownloadHelper.downloadFile(`bandcamp_${date}.txt`, downloadDocument);
7566
});
@@ -84,33 +75,28 @@ export default class DownloadHelper {
8475
}
8576

8677
static generateDownloadList() {
87-
let filelist = "";
88-
document.querySelectorAll("a.item-button").forEach((item, index, list) => {
89-
const url = item.getAttribute("href");
90-
// Prevent duplicate URLs
91-
if (filelist.indexOf(url) === -1) {
92-
filelist += 'curl -OJ "' + url + '" && \\\n';
93-
}
94-
});
95-
filelist = filelist.substring(0, filelist.length - 6);
96-
filelist += "\n";
97-
return filelist;
78+
const urlSet = new Set(
79+
[...document.querySelectorAll("a.item-button")].map(item => {
80+
return item.getAttribute("href");
81+
})
82+
);
83+
84+
if (urlSet.size === 0) return "URLS=()\n";
85+
86+
const fileList = [...urlSet].map(url => `\t"${url}"`).join("\n");
87+
return "URLS=(\n" + fileList + "\n)\n";
9888
}
9989

10090
static callback() {
101-
let allDownloadLinks = document.querySelectorAll(
91+
const allDownloadLinks = document.querySelectorAll(
10292
".download-title .item-button"
10393
);
10494

105-
let linksReady = true;
106-
allDownloadLinks.forEach(function(element, index) {
107-
if (element.style.display === "none") {
108-
linksReady = false;
109-
return;
110-
}
111-
});
95+
const linksReady = [...allDownloadLinks].every(
96+
element => element.style.display !== "none"
97+
);
11298

113-
this.log.info("linksReady", linksReady);
99+
this.log.info(`linksReady: ${linksReady}`);
114100
if (linksReady) {
115101
this.enableButton();
116102
return;
@@ -122,4 +108,77 @@ export default class DownloadHelper {
122108
static getDownloadPreamble() {
123109
return preamble;
124110
}
111+
112+
static getDownloadPostamble() {
113+
return postamble;
114+
}
125115
}
116+
117+
const preamble = `#!/usr/bin/env bash
118+
119+
# Generated by Bandcamp Enhancement Suite (https://github.com/sabjorn/BandcampEnhancementSuite)
120+
#
121+
# The following can be used to batch download your recent purchases.
122+
# NOTE: pasting into terminal may not work properly--please follow new instructions below
123+
#
124+
# Usage (Mac/Linux):
125+
# 1) open Terminal
126+
# 2) move to desired download directory (e.g. \`cd ~/Downloads/bandcamp\`)
127+
# 3) run script (e.g. \`bash <this filename>.txt\`
128+
129+
`;
130+
131+
const postamble = `
132+
DEFAULT_BATCH_SIZE=5
133+
134+
download_file() {
135+
local url="$1"
136+
137+
if curl -L --fail -OJ "$url" 2>/dev/null; then
138+
echo -n "."
139+
return 0
140+
else
141+
echo -n "x"
142+
return 1
143+
fi
144+
}
145+
146+
TOTAL_URLS=\${#URLS[@]}
147+
COMPLETED=0
148+
FAILED=0
149+
BATCH_SIZE=\${1:-$DEFAULT_BATCH_SIZE}
150+
if [ "$BATCH_SIZE" -eq "$DEFAULT_BATCH_SIZE" ] && [ -z "$1" ]; then
151+
echo "note: the BATCH_SIZE can be set with a numerical argument after the command. e.g. bash this_script.txt 10"
152+
fi
153+
154+
echo "Beginning parallel download of $TOTAL_URLS files (batch size: $BATCH_SIZE)"
155+
for ((i=0; i<TOTAL_URLS; i+=BATCH_SIZE)); do
156+
pids=()
157+
for ((j=i; j<i+BATCH_SIZE && j<TOTAL_URLS; j++)); do
158+
download_file "\${URLS[j]}" &
159+
pids+=($!)
160+
done
161+
162+
for pid in "\${pids[@]}"; do
163+
wait $pid
164+
status=$?
165+
if [ $status -eq 0 ]; then
166+
((COMPLETED++))
167+
else
168+
((FAILED++))
169+
fi
170+
done
171+
done
172+
173+
echo ""
174+
if [ $FAILED -eq 0 ]; then
175+
echo "Successfully downloaded $TOTAL_URLS files"
176+
else
177+
echo "$FAILED files failed to download"
178+
fi
179+
echo ""
180+
echo "Press any key to exit..."
181+
read -n 1
182+
183+
exit $FAILED
184+
`;

test/download_helper.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,27 @@ describe("Download Helper", () => {
3131
});
3232

3333
describe("generateDownloadList()", () => {
34-
it("should create a string of curl calls with urls from a.item-button", async () => {
34+
it("should create a URL array with urls from a.item-button", async () => {
3535
createDomNodes(`
3636
<span id="testId" class="download-title">
3737
<a class="item-button" href="url1"></button>
3838
<a class="item-button" href="url2"></button>
3939
<a class="item-button" href="url3"></button>
4040
</span>
4141
`);
42-
const curlCommand =
43-
'curl -OJ "url1" && \\\ncurl -OJ "url2" && \\\ncurl -OJ "url3"\n';
42+
const curlCommand = `URLS=(\n\t"url1"\n\t"url2"\n\t"url3"\n)\n`;
43+
44+
const downloadList = DownloadHelper.generateDownloadList();
45+
expect(downloadList).to.equal(curlCommand);
46+
});
47+
48+
it("should create an empty URL array when no urls from a.item-button", async () => {
49+
createDomNodes(`
50+
<span id="testId" class="download-title">
51+
</span>
52+
`);
53+
const curlCommand = `URLS=()\n`;
54+
4455
const downloadList = DownloadHelper.generateDownloadList();
4556
expect(downloadList).to.equal(curlCommand);
4657
});
@@ -53,28 +64,28 @@ describe("Download Helper", () => {
5364
`);
5465

5566
dh.createButton();
56-
const button = document.querySelector(".downloadall");
67+
const button = document.querySelector(".bes-downloadall");
5768

5869
expect(button != null).to.be.true;
5970
});
6071

6172
it("should not replace existing button at the end of div.download-titles", async () => {
6273
createDomNodes(`
6374
<div id="testId" class="download-titles">
64-
<button class="downloadall fake" id="someId"></button>
75+
<button class="bes-downloadall fake" id="someId"></button>
6576
</div>
6677
`);
6778

6879
{
69-
const button = document.querySelector(".downloadall");
80+
const button = document.querySelector(".bes-downloadall");
7081
const idAttribute = button.getAttribute("id");
7182
expect(idAttribute).to.be.equal("someId");
7283
}
7384

7485
dh.createButton();
7586

7687
{
77-
const button = document.querySelectorAll(".downloadall");
88+
const button = document.querySelectorAll(".bes-downloadall");
7889
assert(button.length == 1);
7990
const idAttribute = button[0].getAttribute("id");
8091
expect(idAttribute).to.be.equal("someId");
@@ -88,7 +99,7 @@ describe("Download Helper", () => {
8899

89100
dh.createButton();
90101

91-
const buttonClassName = "downloadall";
102+
const buttonClassName = "bes-downloadall";
92103
expect(dh.button.className).to.equal(buttonClassName);
93104

94105
const buttonTitleExpected =
@@ -106,6 +117,7 @@ describe("Download Helper", () => {
106117
DownloadHelper.dateString = sinon.fake.returns("dateString");
107118
DownloadHelper.generateDownloadList = sinon.fake.returns("downloadList");
108119
DownloadHelper.getDownloadPreamble = sinon.fake.returns("preamble");
120+
DownloadHelper.getDownloadPostamble = sinon.fake.returns("postamble");
109121
DownloadHelper.downloadFile = sinon.spy();
110122

111123
dh.button = document.createElement("button");
@@ -123,7 +135,7 @@ describe("Download Helper", () => {
123135
"bandcamp_dateString.txt"
124136
);
125137
expect(DownloadHelper.downloadFile.getCall(0).args[1]).to.equal(
126-
"preamble" + "downloadList"
138+
"preamble" + "downloadList" + "postamble"
127139
);
128140
});
129141

0 commit comments

Comments
 (0)