Skip to content

Instantly share code, notes, and snippets.

@RascalTwo
Last active October 26, 2022 07:11
Show Gist options
  • Save RascalTwo/d83eac06da6ec85c4095f146cab55a2e to your computer and use it in GitHub Desktop.
Save RascalTwo/d83eac06da6ec85c4095f146cab55a2e to your computer and use it in GitHub Desktop.

Dynabook (Toshiba) Driver Downloader

Make downloading Dynabook (Toshiba) drivers easier!

dynabook.driver.downloader.mp4

Have you ever needed to all of the latest drivers/resources for your Dynabook laptop?

Of course, so you go to the official site and look to see which drivers to download.

But then you see there are dozens...and each one requires you to go to another page, then click download - such a tedious process!


Then this is for you - both as a Bookmarklet and UserScript - to improve the UI for bulk downloading.

Now only does it allow for you to easily select which ones you wish to download and do so all from the same page.

It also gives you the power to sort by any attribute, so you can easily only download the latest versions you want.

How It's Made

Tech Used: HTML, CSS, JavaScript

First the script parses the page to get all the drivers and their information, then after generating the improved UI, when requested it will fetch the driver URL from the internal API - allowing direct download.

Optimizations

While the script is optimized to only run on the Dynabook webpages, and even when so only begins the computationally expensive parsing when the user requests the URLs, using more of the API would be both more accurate and stable.

Lessons Learned

While not my first Userscript, it was a unique experience operating with the Dynabook Support internal API, which was not documented and required some reverse engineering.

Usage

Bookmarklet

Create a new bookmark, and set the URL/Location to the content of bookmarklet.js.

UserScript

userscript.js is made for Greasemonkey, but should work with any engine that's compatible with Greasemonkey scripts.

Development

Bookmarklet

  • Run the code through a minifier - this one works
  • Add javascript:( prefix
  • Replace end }(); with })();
javascript:(function(){let t=document.querySelector("#driversUpdatesDiv");let e={name:"asc",version:"desc","posted date":"desc",contentid:"desc",size:"desc"};const n=(()=>{const e=t=>parseInt(t)*({b:1,k:1e3,m:1e6,g:1e9}[t[t.length-1]]||1);return()=>t?[...t.querySelectorAll("dt")].map(t=>{const[n,o,c,r]=t.firstChild.getAttribute("onclick").split("(")[1].split(")")[0].split(",").map(t=>t.trim().slice(1,-1).toLowerCase()),l=t.nextSibling.textContent.split("|").map(t=>t.trim().split(":").map(t=>t.trim().toLowerCase())).reduce((t,[n,o])=>({...t,[n]:"size"===n?e(o):o}),{});return{dt:t,contenttype:n,contentid:o,cipherkey:c,sor:r,name:t.textContent,checked:!1,...l,sortables:["name",...Object.keys(l),"contentid"]}}):[]})();let o=n(),c=!1,r=o.flatMap(t=>t.sortables).filter((t,e,n)=>n.indexOf(t)===e);const[l,s]=(()=>{const t={loading:!1};return[t,async e=>{t.loading=!0,await Promise.resolve(e()).catch(console.error),t.loading=!1}]})(),a=t=>null==t,i=()=>o.sort((t,n)=>{let o=0;for(const[c,r]of Object.entries(e)){const e=[t,n].map(t=>t[c]);if(e[0]===e[1])continue;const l=e.map(a);if(o=l.some(Boolean)?a(l[0])?1:-1:e[0]===e.slice().sort("number"==typeof e[0]?(t,e)=>t-e:void 0)[0]?-1:1,"desc"===r&&(o=-o),o)return o}return o}),d=({target:t})=>{if(l.loading)return;const e=t.closest("th.sortable");if(e)return b(e,e.dataset.sortable);if(t.closest("summary"))return c=!c;const n=t.closest("[data-contentid]"),r=n?n.dataset.contentid:null,a=r?o.find(t=>t.contentid===r):null,i=t.closest("a");if(i)return a.downloaded=!0,i.style.textDecoration="line-through";if(t.closest('input[type="checkbox"]'))return a.checked=!a.checked;const d=t.closest("button");return d?s(()=>({"driver-action":()=>{const t=o.find(t=>t.contentid===r);t.url?y(t.url):v(t)},"generate-selected":()=>v(...o.filter(t=>t.checked)),"download-selected":()=>y(...o.filter(t=>t.checked&&t.url).map(t=>t.url)),"select-all":()=>{o.forEach(t=>t.checked=!0),m()},"unselect-all":()=>{o.forEach(t=>t.checked=!1),m()},"invert-selection":()=>{o.forEach(t=>t.checked=!t.checked),m()},"select-recommended":()=>{const t=new Set(k());o.forEach(e=>e.checked=t.has(e.contentid)),m()}}[d.className]||(()=>void 0))()):void 0},u=()=>{const t=o.filter(t=>t.url);return`\n\t\t\t<div>\n\t\t\t\t<button class="generate-selected">Generate Selected URLs</button>\n\t\t\t\t<br/>\n\t\t\t\t\x3c!--<button class="download-selected">Download Selected</button>\n\t\t\t\t<br/>--\x3e\n\t\t\t\t<button class="select-all">Select All</button>\n\t\t\t\t<br/>\n\t\t\t\t<button class="unselect-all">Unselect All</button>\n\t\t\t\t<br/>\n\t\t\t\t<button class="invert-selection">Invert Selection</button>\n\t\t\t\t<br/>\n\t\t\t\t<button class="select-recommended">Select Recommended</button>\n\t\t\t\t<br/>\n\t\t\t\t<details${c?' open="true"':""}>\n\t\t\t\t\t<summary>${t.length} URLs</summary>\n\n\t\t\t\t\t<ul style="text-align: left;">\n\t\t\t\t\t\t${t.map(t=>`<li data-contentid="${t.contentid}"><a download target="_blank" href="${t.url}"${t.downloaded?' style="text-decoration: line-through;"':""}>${t.url}</a></li>`).join("\n")}\n\t\t\t\t\t</ul>\n\t\t\t\t</details>\n\t\t\t</div>\n\t\t`},m=()=>s(()=>{const t=document.getElementById("driversUpdatesDiv");if(!t)return!1;t.innerHTML="";const n=(t=>{const e=document.createElement("template");return e.innerHTML=t.trim(),e.content.firstChild})(`\n\t\t\t<div id="driversUpdatesDiv" style="text-align: center;">\n\t\t\t\t${u()}\n\t\t\t\t${(t=>`\n\t\t<table style="table-layout: fixed; width: 650px; border-collapse: collapse !important;">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>Download</th>\n\t\t\t\t\t${t.map(t=>`\n\t\t\t\t\t\t<th class="sortable" data-sortable="${t}">\n\t\t\t\t\t\t\t${(t=>t.length?t[0].toUpperCase()+t.slice(1):t)(t)}\n\t\t\t\t\t\t\t<br/>\n\t\t\t\t\t\t\t${({asc:"▲",desc:"▼"}[e[t]]||"").repeat(Object.keys(e).indexOf(t)+1)}\n\t\t\t\t\t\t</th>\n\t\t\t\t\t`).join("\n")}\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t${o.map(e=>`\n\t\t\t\t\t<tr data-contentid="${e.contentid}">\n\t\t\t\t\t\t<td style="border: 1px solid black; vertical-align: middle;">\n\t\t\t\t\t\t\t<input type="checkbox" style="width: 100%" ${e.checked?"checked":""}/>\n\t\t\t\t\t\t\t<button class="driver-action">${e.url?"Download":"Generate"}</button>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t${t.map(t=>`<td style="border: 1px solid black; word-wrap: break-word; vertical-align: middle;">${e[t]||""}</td>`).join("\n")}\n\t\t\t\t\t</tr>\n\t\t\t\t`).join("\n")}\n\t\t\t</tbody>\n\t\t</table>\n\t`)(r)}\n\t\t\t\t${u()}\n\t\t\t</div>\n\t\t`);return n.addEventListener("click",d),t.replaceWith(n),!0}),p=[void 0,"asc","desc"],b=(t,n)=>s(()=>{const t=e[n],o=p[(p.indexOf(t)+1)%p.length];o?e[n]=o:delete e[n],i(),m()}),h=t=>t.split("/").slice(-1)[0],f=t=>new Promise(e=>setTimeout(e,t)),y=(...t)=>s(async()=>{for(const e of t){const t=document.createElement("a");document.body.appendChild(t),t.download=h(e),t.href=e,t.click(),await f(1e3),t.remove()}}),v=(...t)=>s(async()=>{for(const e of t){const t=await fetch(`https://support.dynabook.com/support/contentDetail?contentType=${e.contenttype}&contentId=${e.contentid}&cipherKey=${e.cipherkey}&sor=${e.sor}`).then(t=>t.json());e.url=t.contentFile,await m()}return m()}),k=()=>Object.values(o.reduce((t,e)=>({...t,[e.name]:[...t[e.name]||[],e]}),{})).map(t=>t[0].contentid);(async()=>{for(;;){if(await f(500),!(t=document.querySelector("#driversUpdatesDiv")))continue;if(await f(500),!(o=n()).length)continue;if(r=o.flatMap(t=>t.sortables).filter((t,e,n)=>n.indexOf(t)===e),await f(500),i(),k().forEach(t=>o.find(e=>e.contentid===t).checked=!0),!m())continue;const e=document.getElementById("driversFilterOptionDiv");e&&e.remove();break}})().catch(console.error)})();
(function(){
let WRAPPER = document.querySelector('#driversUpdatesDiv');
const INITIAL_SORTING_PROPERTIES = {
'name': 'asc',
'version': 'desc',
'posted date': 'desc',
'contentid': 'desc',
'size': 'desc'
};
let sortingProperties = {...INITIAL_SORTING_PROPERTIES};
const parseDrivers = (() => {
const parseSize = (rawSize) => parseInt(rawSize) * ({
'b': 1,
'k': 1000,
'm': 1000 * 1000,
'g': 1000 * 1000 * 1000,
}[rawSize[rawSize.length - 1]] || 1);
return () => WRAPPER ? [...WRAPPER.querySelectorAll('dt')].map(dt => {
const [contenttype, contentid, cipherkey, sor] = dt.firstChild.getAttribute('onclick').split('(')[1].split(')')[0].split(',')
.map(arg => arg.trim().slice(1, -1).toLowerCase())
const detailMap = dt.nextSibling.textContent.split('|')
.map(rawDetail => rawDetail.trim().split(':').map(part => part.trim().toLowerCase()))
.reduce((map, [key, rawValue]) => ({
...map,
[key]: key === 'size' ? parseSize(rawValue) : rawValue
}), {});
return {
dt, contenttype, contentid, cipherkey, sor,
name: dt.textContent,
checked: false,
...detailMap,
sortables: [
'name',
...Object.keys(detailMap),
'contentid',
]
};
}) : [];
})();
let drivers = parseDrivers();
let detailsOpen = false;
let ALL_SORTABLES = drivers.flatMap(driver => driver.sortables).filter((sortable, i, arr) => arr.indexOf(sortable) === i);
const [state, load] = (() => {
const state = {
loading: false
};
return [
state,
async callback => {
state.loading = true;
await Promise.resolve(callback()).catch(console.error);
state.loading = false;
}
];
})();
const isNullOrUndefined = target => target === undefined || target === null;
const sortDrivers = () => drivers.sort((a, b) => {
let result = 0;
for (const [prop, direction] of Object.entries(sortingProperties)){
const values = [a, b].map(v => v[prop]);
if (values[0] === values[1]) continue;
const missingValues = values.map(isNullOrUndefined);
if (missingValues.some(Boolean)){
result = isNullOrUndefined(missingValues[0]) ? 1 : -1;
}
else{
result = values[0] === values.slice().sort(typeof values[0] === 'number' ? (a, b) => a - b : undefined)[0] ? -1 : 1;
}
if (direction === 'desc') result = -result;
if (result) return result;
}
return result;
});
const titleize = string => string.length ? string[0].toUpperCase() + string.slice(1) : string;
const htmlToElement = html => {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
};
const generateTable = allSortables => `
<table style="table-layout: fixed; width: 650px; border-collapse: collapse !important;">
<thead>
<tr>
<th>Download</th>
${allSortables.map(sortable => `
<th class="sortable" data-sortable="${sortable}">
${titleize(sortable)}
<br/>
${({'asc': '▲', 'desc': '▼'}[sortingProperties[sortable]] || '').repeat(Object.keys(sortingProperties).indexOf(sortable) + 1)}
</th>
`).join('\n')}
</tr>
</thead>
<tbody>
${drivers.map(driver => `
<tr data-contentid="${driver.contentid}">
<td style="border: 1px solid black; vertical-align: middle;">
<input type="checkbox" style="width: 100%" ${driver.checked ? 'checked' : ''}/>
<button class="driver-action">${driver.url ? 'Download' : 'Generate'}</button>
</td>
${allSortables.map(sortable => `<td style="border: 1px solid black; word-wrap: break-word; vertical-align: middle;">${driver[sortable] || ''}</td>`).join('\n')}
</tr>
`).join('\n')}
</tbody>
</table>
`;
const handleClick = ({ target }) => {
if (state.loading) return;
const th = target.closest('th.sortable');
if (th) return handleSortingChange(th, th.dataset.sortable);
const summary = target.closest('summary');
if (summary) return detailsOpen = !detailsOpen;
const wrapper = target.closest('[data-contentid]')
const contentid = wrapper ? wrapper.dataset.contentid : null;
const driver = contentid ? drivers.find(driver => driver.contentid === contentid) : null;
const anchor = target.closest('a');
if (anchor) {
driver.downloaded = true;
return anchor.style.textDecoration = 'line-through';
}
const checkbox = target.closest('input[type="checkbox"]');
if (checkbox) return driver.checked = !driver.checked;
const button = target.closest('button');
if (!button) return;
return load(() => ({
'driver-action': () => {
const driver = drivers.find(driver => driver.contentid === contentid);
if (driver.url) downloadURLs(driver.url);
else generateURLs(driver);
},
'generate-selected': () => generateURLs(...drivers.filter(driver => driver.checked)),
'download-selected': () => downloadURLs(...drivers.filter(driver => driver.checked && driver.url).map(driver => driver.url)),
'select-all': () => {
drivers.forEach(driver => driver.checked = true);
renderLayout();
},
'unselect-all': () => {
drivers.forEach(driver => driver.checked = false);
renderLayout();
},
'invert-selection': () => {
drivers.forEach(driver => driver.checked = !driver.checked);
renderLayout();
},
'select-recommended': () => {
const ids = new Set(calculateRecommendedIDs());
drivers.forEach(driver => driver.checked = ids.has(driver.contentid));
renderLayout();
}
}[button.className] || (() => undefined))());
};
const generateControls = () => {
const generatedDrivers = drivers.filter(driver => driver.url);
return `
<div>
<button class="generate-selected">Generate Selected URLs</button>
<br/>
<!--<button class="download-selected">Download Selected</button>
<br/>-->
<button class="select-all">Select All</button>
<br/>
<button class="unselect-all">Unselect All</button>
<br/>
<button class="invert-selection">Invert Selection</button>
<br/>
<button class="select-recommended">Select Recommended</button>
<br/>
<details${detailsOpen ? ' open="true"' : ''}>
<summary>${generatedDrivers.length} URLs</summary>
<ul style="text-align: left;">
${generatedDrivers.map(driver => `<li data-contentid="${driver.contentid}"><a download target="_blank" href="${driver.url}"${driver.downloaded ? ' style="text-decoration: line-through;"' : ''}>${driver.url}</a></li>`).join('\n')}
</ul>
</details>
</div>
`;
};
const renderLayout = () => load(() => {
const target = document.getElementById('driversUpdatesDiv');
if (!target) return false;
target.innerHTML = '';
const container = htmlToElement(`
<div id="driversUpdatesDiv" style="text-align: center;">
${generateControls()}
${generateTable(ALL_SORTABLES)}
${generateControls()}
</div>
`)
container.addEventListener('click', handleClick);
target.replaceWith(container);
return true;
})
const directions = [undefined, 'asc', 'desc'];
const handleSortingChange = (th, sortable) => load(() => {
const currentDirection = sortingProperties[sortable];
const nextDirection = directions[(directions.indexOf(currentDirection) + 1) % directions.length];
if (!nextDirection) delete sortingProperties[sortable];
else sortingProperties[sortable] = nextDirection;
sortDrivers();
renderLayout();
})
const getURLFilename = url => url.split('/').slice(-1)[0];
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// Doesn't work on 100% of URLs when passed multiple
const downloadURLs = (...urls) => load(async () => {
for (const url of urls){
const anchor = document.createElement('a');
document.body.appendChild(anchor);
anchor.download = getURLFilename(url);
anchor.href = url;
anchor.click();
await delay(1000);
anchor.remove();
}
});
const generateURLs = (...drivers) => load(async () => {
for (const driver of drivers){
const json = await fetch(`https://support.dynabook.com/support/contentDetail?contentType=${driver.contenttype}&contentId=${driver.contentid}&cipherKey=${driver.cipherkey}&sor=${driver.sor}`).then(r => r.json());
driver.url = json.contentFile;
await renderLayout();
}
return renderLayout();
})
const calculateRecommendedIDs = () => Object.values(drivers.reduce((map, driver) => ({
...map,
[driver.name]: [...(map[driver.name] || []), driver]
}), {})).map(options => options[0].contentid);
(async () => {
while(true){
await delay(500);
WRAPPER = document.querySelector('#driversUpdatesDiv');
if (!WRAPPER) continue;
await delay(500);
drivers = parseDrivers();
if (!drivers.length) continue;
ALL_SORTABLES = drivers.flatMap(driver => driver.sortables).filter((sortable, i, arr) => arr.indexOf(sortable) === i);
await delay(500);
sortDrivers();
calculateRecommendedIDs().forEach(id => drivers.find(driver => driver.contentid === id).checked = true);
if (!renderLayout()) continue;
const filterDiv = document.getElementById('driversFilterOptionDiv')
if (filterDiv) filterDiv.remove();
break;
}
})().catch(console.error);
})();
// ==UserScript==
// @name Dynabook Driver Downloader
// @version 1
// @grant none
// @match *://support.dynabook.com/support/*
// @run-at document-end
// ==/UserScript==
// PASTE source.js HERE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment