// ==UserScript==
// @name Better Upload
// @description Enhances the process of uploading mp3 files to Deezer by providing a better UI and handling for uploads.
// @author bertigert
// @version 1.0.1
// @icon https://www.google.com/s2/favicons?sz=64&domain=deezer.com
// @namespace Violentmonkey Scripts
// @match https://www.deezer.com/*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js
// ==/UserScript==
(function() {
"use strict";
const jsmediatags = window.jsmediatags || require("./deps/jsmediatags3.9.5.min.js");
class Logger {
static LOG_VERY_MANY_THINGS_YES_YES = true; // set to false if you dont want the console getting spammed
constructor() {
this.log_textarea = null;
this.PREFIXES = Object.freeze({
INFO: "?",
WARN: "⚠",
ERROR: "!",
SUCCESS: "*",
CONSOLE: "[Better Upload]"
});
this.console = {
log: (...args) => console.log(this.PREFIXES.CONSOLE, ...args),
warn: (...args) => console.warn(this.PREFIXES.CONSOLE, ...args),
error: (...args) => console.error(this.PREFIXES.CONSOLE, ...args),
debug: (...args) => {if (Logger.LOG_VERY_MANY_THINGS_YES_YES) console.debug(this.PREFIXES.CONSOLE, ...args)}
};
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function is_url_correct(url) {
const regex = /\/(?:[^\/]*)\/profile\/\d+\/personal_song/; // this matches both the pathname of the website and the hash of the desktop app
return regex.test(url || window.location.href);
}
class Deezer {
constructor() {
this.session_id = null;
this.api_token = null;
}
async get_user_data(c=0) {
const r = await fetch("https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=", {
"body": "{}",
"method": "POST",
"credentials": "include"
});
if (!r.ok) {
return null;
}
const resp = await r.json();
if (!resp?.results?.SESSION_ID) {
if (c < 3) {
await sleep(2000);
return this.get_user_data(c+1);
}
logger.console.error("Failed to get data after 3 attempts");
return null;
}
this.session_id = resp.results.SESSION_ID;
this.api_token = resp.results.checkForm;
return true;
}
async get_personal_songs() {
if (!this.api_token) {
await this.get_user_data();
if (!this.api_token) {
return new Set();
}
}
const r = await fetch(`https://www.deezer.com/ajax/gw-light.php?method=personal_song.getList&input=3&api_version=1.0&api_token=${this.api_token}&cid=${Math.floor(Math.random()*1e9)}`, {
"body": "{\"nb\":2000,\"start\":0}",
"method": "POST",
"mode": "cors",
"credentials":"include"
});
const upload_id_set = new Set();
if (r.ok) {
const resp = await r.json();
if (resp.error?.length === 0 && resp.results?.data) {
for (const song of resp.results.data) {
upload_id_set.add(song.UPLOAD_ID);
}
}
}
return upload_id_set;
}
async upload_file(file, info_item) {
if (!this.session_id) {
await this.get_user_data();
if (!this.session_id) {
return { success: false, upload_id: null };
}
}
const url = `https://upload.deezer.com/?sid=${this.session_id}&id=0&resize=1&directory=user&type=audio&referer=FR&file=${encodeURIComponent(file.name)}`;
const formData = new FormData();
formData.append("file", file, file.name);
let startTime = Date.now();
let timerId = null;
if (info_item.elapsed) {
info_item.elapsed.textContent = "0s";
timerId = setInterval(() => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
info_item.elapsed.textContent = `${elapsed}s`;
}, 100);
}
try {
const resp = await fetch(url, {
method: "POST",
body: formData,
signal: AbortSignal.timeout(config.timeout*1000 || 30000)
});
if (timerId) clearInterval(timerId);
if (info_item.elapsed) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
info_item.elapsed.textContent = `${elapsed}s`;
}
if (resp.ok) {
const data = await resp.json();
let upload_id = null;
if (data && data.error?.length === 0) {
upload_id = data.results;
return { success: true, upload_id };
} else {
logger.console.error("Upload failed:", data);
return { success: false, upload_id };
}
} else {
return { success: false, upload_id: null };
}
} catch (e) {
if (timerId) clearInterval(timerId);
if (info_item.elapsed) {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
info_item.elapsed.textContent = `${elapsed}s`;
}
if (e.name !== "TimeoutError") {
logger.console.error("Upload error:", e);
}
return { success: false, upload_id: null };
}
}
}
class Setting {
// only the constructor and 1 function should be called per instance of this class
constructor(name, description, config_key_parent, config_key) {
// we take advantage of the fact that objects (the parent) are passed by reference so we can modify the original config
this.config_key_parent = config_key_parent;
this.config_key = config_key;
this.setting_label = document.createElement("label");
this.setting_label.title = description;
const setting_name = document.createElement("span");
setting_name.textContent = name;
this.setting_label.appendChild(setting_name);
}
text_setting(modify_value_callback=null, additional_callback=null) {
const setting_input = document.createElement("textarea");
setting_input.value = this.config_key_parent[this.config_key];
setting_input.onchange = () => {
this.config_key_parent[this.config_key] = modify_value_callback ? modify_value_callback(setting_input.value) : setting_input.value;
if (additional_callback) additional_callback(this.config_key_parent[this.config_key]);
}
this.setting_label.appendChild(setting_input);
return this.setting_label;
}
number_setting(modify_value_callback=null, additional_callback=null, range=[null, null, null]) {
const setting_input = document.createElement("input");
setting_input.type = "number";
if (setting_input.min) setting_input.min = range[0];
if (setting_input.max) setting_input.max = range[1];
if (setting_input.step) setting_input.step = range[2];
setting_input.value = this.config_key_parent[this.config_key];
setting_input.onchange = () => {
this.config_key_parent[this.config_key] = modify_value_callback ? modify_value_callback(setting_input.value) : parseInt(setting_input.value);
if (additional_callback) additional_callback(this.config_key_parent[this.config_key]);
}
this.setting_label.appendChild(setting_input);
return this.setting_label;
}
checkbox_setting(modify_value_callback=null, additional_callback=null) {
const setting_input = document.createElement("input");
setting_input.type = "checkbox";
setting_input.checked = this.config_key_parent[this.config_key];
setting_input.onchange = () => {
this.config_key_parent[this.config_key] = modify_value_callback ? modify_value_callback(setting_input.checked) : setting_input.checked;
if (additional_callback) additional_callback(this.config_key_parent[this.config_key]);
};
this.setting_label.appendChild(setting_input);
return this.setting_label;
}
dropdown_setting(option_names, modify_value_callback=null, additional_callback=null) {
// options: [nameforoption1, nameforoption2...]
const setting_input = document.createElement("select");
setting_input.className = "release_radar_dropdown";
for (let option_name of option_names) {
const option_elem = document.createElement("option");
option_elem.textContent = option_name;
setting_input.appendChild(option_elem);
}
setting_input.selectedIndex = this.config_key_parent[this.config_key];
setting_input.onchange = () => {
this.config_key_parent[this.config_key] = modify_value_callback ? modify_value_callback(setting_input.selectedIndex) : setting_input.selectedIndex;
if (additional_callback) additional_callback(this.config_key_parent[this.config_key]);
}
this.setting_label.appendChild(setting_input);
return this.setting_label;
}
button_setting(text, on_click) {
const setting_input = document.createElement("button");
setting_input.textContent = text;
setting_input.onclick = () => {on_click(setting_input)};
this.setting_label.appendChild(setting_input);
return this.setting_label;
}
}
class UI {
static funcs = {
upload_file: async (files, info_container, progress_bar_elem, info_list_elem, status_elem) => {
if (!files || files.length === 0) {
return;
}
info_container.classList.remove("better-upload-hidden");
info_list_elem.innerHTML = "";
progress_bar_elem.style.width = "0%";
status_elem.text.textContent = "Uploading files...";
const info_items = [];
const failed_files = [];
let successful_uploads = 0;
let failed_uploads = 0;
for (const file of files) {
const info_item = UI.funcs.create_info_item(
file.name,
`${(file.size / 1e6).toFixed(2)} MB`,
"",
"⏳",
"",
file // pass file object for image extraction
);
info_list_elem.appendChild(info_item.element);
info_items.push(info_item);
info_list_elem.scrollTop = info_list_elem.scrollHeight;
}
const total_files = files.length;
const batch_size = config.batch_size || 1;
let uploaded_files = 0;
let next_file_index = batch_size;
const process_one = async (index) => {
const file = files[index];
const info_item = info_items[index];
info_item.status.textContent = "⬆️";
info_item.status.classList.replace("better-upload-waiting", "better-upload-uploading");
const result = await deezer.upload_file(file, info_item);
info_item.status.classList.remove("better-upload-uploading");
if (result.success) {
info_item.status.textContent = "✔️";
info_item.song_id.textContent = `${result.upload_id}`;
if (personal_songs.has(result.upload_id)) {
info_item.element.title = "This file has already been uploaded once before.";
info_item.element.classList.add("better-upload-already-uploaded");
}
successful_uploads++;
} else {
info_item.element.classList.add("better-upload-error");
info_item.status.textContent = "❌";
failed_uploads++;
failed_files.push(file);
}
uploaded_files++;
const progress = (uploaded_files / total_files) * 100;
progress_bar_elem.style.width = `${progress}%`;
UI.funcs.update_status(
info_container,
progress_bar_elem,
info_list_elem,
status_elem,
total_files,
successful_uploads,
failed_uploads,
failed_files
);
if (next_file_index < total_files) {
await process_one(next_file_index++);
}
}
const personal_songs = await deezer.get_personal_songs();
const starters = [];
for (let i = 0; i < Math.min(batch_size, total_files); i++) {
starters.push(process_one(i));
}
await Promise.all(starters);
},
create_span: (text, class_name) => {
const span = document.createElement("span");
span.textContent = text;
if (class_name) {
span.className = class_name;
}
return span;
},
create_info_item: (file_name, file_size, elapsed, status, song_id, file_obj) => {
const li = document.createElement("li");
li.className = "better-upload-info-item";
const name_container = document.createElement("div");
name_container.className = "better-upload-name-container";
const img_elem = document.createElement("img");
img_elem.className = "better-upload-file-image";
img_elem.src = "https://cdn-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/40x40-000000-80-0-0.jpg";
const name_span = UI.funcs.create_span(file_name);
name_container.append(img_elem, name_span);
jsmediatags.read(file_obj, {
onSuccess: function(tag) {
const pic = tag.tags.picture;
if (pic) {
const byteArray = new Uint8Array(pic.data);
const blob = new Blob([byteArray], { type: pic.format });
img_elem.src = URL.createObjectURL(blob);
img_elem.onload = () => URL.revokeObjectURL(img_elem.src);
}
},
onError: function(error) {
return;
}
});
const size_elem = UI.funcs.create_span(file_size);
const elapsed_elem = UI.funcs.create_span(elapsed);
const status_elem = UI.funcs.create_span(status);
status_elem.className = "better-upload-status-icon better-upload-waiting";
const song_id_elem = UI.funcs.create_span(song_id);
li.append(name_container, size_elem, elapsed_elem, song_id_elem, status_elem);
return {
element: li,
name: name_container,
size: size_elem,
elapsed: elapsed_elem,
status: status_elem,
song_id: song_id_elem
};
},
create_status_element: () => {
const status_container = document.createElement("div");
status_container.className = "better-upload-status";
const status_text = UI.funcs.create_span("Ready to upload. You should not see this.");
const retry_button = document.createElement("button");
retry_button.className = "better-upload-action-button better-upload-hidden";
retry_button.textContent = "Retry Failed";
const reload_button = document.createElement("button");
reload_button.className = "better-upload-action-button";
reload_button.textContent = "Reload Page";
reload_button.onclick = () => location.reload();
status_container.append(status_text, retry_button, reload_button);
return {
container: status_container,
text: status_text,
retry_button: retry_button,
reload_button: reload_button
};
},
update_status: (info_container, progress_bar_elem, info_list_elem, status_elem, total, successful, failed, failed_files) => {
let status_text = `Successfully Uploaded: ${successful}/${total}`;
if (failed > 0) {
status_text += ` (${failed} failed)`;
status_elem.retry_button.classList.remove("better-upload-hidden");
status_elem.retry_button.onclick = () => {
if (successful+failed !== total) {
return;
}
status_elem.retry_button.classList.add("better-upload-hidden");
status_elem.text.textContent = "Retrying failed uploads...";
UI.funcs.upload_file(failed_files, info_container, progress_bar_elem, info_list_elem, status_elem);
};
} else {
status_elem.retry_button.classList.add("better-upload-hidden");
}
status_elem.text.textContent = status_text;
}
}
static has_created_ui = false;
static create_ui() {
const selector = "#page_profile > div.naboo-catalog-content-wrapper > div.naboo-catalog-content > div[role='tabpanel'] > div.container";
const own_ui_selector = "div.better-upload-container";
let parent = document.querySelector(selector);
if (parent) {
if (parent.querySelector(own_ui_selector)) return;
UI.has_created_ui = false;
UI.entry_point(parent);
logger.console.debug("UI created");
} else {
UI.has_created_ui = false;
logger.console.debug("Waiting for parent");
const observer = new MutationObserver(mutations => {
for (let mutation of mutations) {
if (mutation.type === 'childList') {
parent = document.querySelector(selector);
if (parent) {
observer.disconnect();
if (parent.querySelector(own_ui_selector)) return;
if (UI.entry_point(parent)) logger.console.debug("UI created");
}
}
}
});
observer.observe(document.body, {childList: true, subtree: true});
}
}
static ensure_ui() {
if (is_url_correct()) {
UI.create_ui();
}
window.history.pushState = new Proxy(window.history.pushState, {
apply: (target, thisArg, argArray) => {
if (is_url_correct(argArray[2])) {
UI.create_ui();
}
return target.apply(thisArg, argArray);
},
});
window.addEventListener("popstate", (e) => {
if (is_url_correct()) {
UI.create_ui();
}
});
}
static entry_point(parent) {
if (UI.has_created_ui) return;
UI.has_created_ui = true;
UI.create_css();
const container = UI.create_container(parent);
parent.appendChild(container);
return true;
}
static create_container(parent) {
const container = document.createElement("div");
container.className = "better-upload-container";
const elements = UI.create_elements(parent);
container.append(...elements);
return container;
}
static create_elements(parent) {
const toolbar = parent.querySelector("div.loved-heading > div[data-testid='toolbar']");
if (!toolbar) {
logger.console.warn("Toolbar not found, cannot create Better Upload UI");
return [];
}
const settings_header = document.createElement("div");
settings_header.className = "better-upload-settings-header better-upload-hidden";
const async_limit_setting = new Setting(
"Batch Size",
"Number of files to upload in parallel",
config, "batch_size",
).number_setting(null, null, [1, null, 1]);
const timeout_setting = new Setting(
"Timeout",
"Timeout for each upload in seconds",
config, "timeout",
).number_setting(null, null, [1, null, 1]);
settings_header.append(async_limit_setting, timeout_setting);
const settings_button = document.createElement("button");
settings_button.className = "better-upload-settings-button";
settings_button.innerHTML = `
<svg focusable="false" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
d="m14.61 4.122 1.116.462c.748.31 1.142 1.13.916 1.907l-.284.98a.13.13 0 0 0 .036.13.143.143 0 0 0 .1.046.125.125 0 0 0 .036-.005l.98-.284a1.584 1.584 0 0 1 1.907.916l.462 1.116c.31.748.008 1.607-.7 1.997l-.894.493a.13.13 0 0 0-.067.114c0 .061.025.104.066.127l.894.492c.71.39 1.01 1.25.7 1.997l-.461 1.116a1.584 1.584 0 0 1-1.908.916l-.98-.284a.13.13 0 0 0-.129.036c-.042.042-.055.09-.042.136l.284.98a1.585 1.585 0 0 1-.916 1.907l-1.116.461a1.585 1.585 0 0 1-1.997-.7l-.492-.893a.13.13 0 0 0-.115-.067c-.061 0-.104.025-.126.066l-.493.894a1.586 1.586 0 0 1-1.997.7l-1.116-.461a1.585 1.585 0 0 1-.916-1.908l.284-.98a.13.13 0 0 0-.036-.128.146.146 0 0 0-.102-.047.123.123 0 0 0-.034.004l-.98.284a1.582 1.582 0 0 1-1.907-.916l-.462-1.116a1.585 1.585 0 0 1 .7-1.997l.894-.492a.13.13 0 0 0 .066-.114c0-.061-.024-.104-.065-.127l-.894-.493a1.585 1.585 0 0 1-.7-1.997l.461-1.115a1.587 1.587 0 0 1 1.908-.917l.98.284a.132.132 0 0 0 .129-.036c.042-.042.055-.09.042-.135l-.284-.98a1.585 1.585 0 0 1 .916-1.907l1.116-.462a1.585 1.585 0 0 1 1.997.7l.492.894c.014.026.047.037.075.047a.134.134 0 0 1 .04.02c.061 0 .104-.025.126-.066l.493-.895a1.584 1.584 0 0 1 1.997-.7Zm2.29 4.8-.405.058a1.47 1.47 0 0 1-1.04-.433 1.463 1.463 0 0 1-.378-1.445l.285-.982a.253.253 0 0 0-.146-.304L14.1 5.354a.252.252 0 0 0-.32.111l-.492.895a1.47 1.47 0 0 1-1.294.755c-.564.005-1.047-.27-1.284-.76l-.49-.89a.252.252 0 0 0-.323-.11l-1.113.46a.253.253 0 0 0-.146.305l.284.98a1.474 1.474 0 0 1-.38 1.45 1.45 1.45 0 0 1-1.036.43L7.1 8.923l-.982-.284a.254.254 0 0 0-.305.147L5.353 9.9a.253.253 0 0 0 .112.32l.894.492c.47.26.759.759.755 1.303 0 .527-.29 1.02-.759 1.275l-.89.49a.254.254 0 0 0-.112.32l.462 1.116a.252.252 0 0 0 .303.147l.981-.285.405-.057c.387 0 .755.152 1.037.429.385.38.53.935.382 1.449l-.285.982a.253.253 0 0 0 .147.304l1.115.462a.25.25 0 0 0 .32-.112l.492-.894a1.47 1.47 0 0 1 1.294-.755c.535 0 1.027.29 1.284.758l.49.891a.254.254 0 0 0 .32.112l1.116-.462a.254.254 0 0 0 .146-.305l-.284-.979a1.474 1.474 0 0 1 .38-1.45 1.458 1.458 0 0 1 1.036-.43l.386.051 1 .29a.251.251 0 0 0 .305-.146l.462-1.116a.254.254 0 0 0-.112-.32l-.894-.491a1.473 1.473 0 0 1-.755-1.303c0-.527.29-1.019.758-1.275l.891-.491a.253.253 0 0 0 .112-.32l-.462-1.115a.252.252 0 0 0-.306-.146l-.978.284ZM9 12c0-1.927 1.073-3 3-3s3 1.073 3 3c0 1.926-1.073 3-3 3s-3-1.074-3-3Zm1.333 0c0 1.184.483 1.667 1.667 1.667 1.184 0 1.667-.483 1.667-1.667 0-1.184-.483-1.667-1.667-1.667-1.184 0-1.667.483-1.667 1.667Z"
clip-rule="evenodd">
</path>
</svg>`;
settings_button.onclick = () => {
settings_header.classList.toggle("better-upload-hidden");
}
toolbar.appendChild(settings_button);
const info_container = document.createElement("div");
info_container.className = "better-upload-info-container better-upload-hidden";
const progress_bar = document.createElement("div");
progress_bar.className = "better-upload-progress-bar";
const info_header = document.createElement("div");
info_header.className = "better-upload-info-header";
info_header.append(
UI.funcs.create_span("File"),
UI.funcs.create_span("Size"),
UI.funcs.create_span("Elapsed"),
UI.funcs.create_span("ID"),
UI.funcs.create_span("")
);
const info_list = document.createElement("ul");
info_list.className = "better-upload-info-list";
const status_element = UI.funcs.create_status_element();
info_container.append(progress_bar, info_header, info_list, status_element.container);
// replace original upload input with our own
const file_upload_input = document.createElement("input");
file_upload_input.type = "file";
file_upload_input.multiple = true;
file_upload_input.accept = "audio/mp3,audio/mpeg";
file_upload_input.className = "better-upload-hidden";
let is_processing = false;
file_upload_input.onchange = async () => {
if (is_processing) return;
is_processing = true;
await UI.funcs.upload_file(file_upload_input.files, info_container, progress_bar, info_list, status_element);
is_processing = false;
}
const orig_upload_input = toolbar.querySelector("input[data-testid='upload-file']");
orig_upload_input.parentNode.querySelector("button").onclick = (e) => {
e.stopPropagation();
file_upload_input.click();
}
orig_upload_input.replaceWith(file_upload_input);
return [settings_header, info_container];
}
static create_css() {
const grid_template_columns = "12fr 3fr 3fr 4fr 1fr";
const css = `
.better-upload-hidden {
display: none !important;
}
div.better-upload-container {
margin-top: 10px;
display: flex;
flex-direction: column;
}
button.better-upload-settings-button > svg {
width: 48px;
height: 48px;
fill: var(--tempo-colors-text-neutral-primary-default);
}
@keyframes spin180 {
from { transform: rotate(0deg); }
to { transform: rotate(180deg); }
}
button.better-upload-settings-button:hover > svg {
animation: spin180 0.5s ease-in-out;
}
div.better-upload-settings-header {
width: 50%;
display: flex;
flex-direction: column;
margin: 10px 0px;
overflow: auto;
background: var(--tempo-colors-background-neutral-secondary-default);
border-radius: 10px;
padding: 5px;
}
div.better-upload-settings-header > label {
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
color: var(--tempo-colors-text-neutral-primary-default);
margin: 5px;
}
div.better-upload-settings-header > label > * {
width: 50%;
}
div.better-upload-settings-header > label > input {
margin-left: 6px;
min-width: 0;
flex: 1;
padding: 4px 10px;
border: 1px solid transparent;
border-radius: 5px;
background: var(--tempo-colors-background-neutral-tertiary-default);
color: var(--tempo-colors-text-neutral-primary-default);
font-size: 14px;
}
div.better-upload-settings-header > label > input[type='checkbox'] {
accent-color: var(--tempo-colors-border-neutral-primary-focused);
width: 20px;
height: 20px;
}
div.better-upload-settings-header > label > input:hover {
background: var(--tempo-colors-background-neutral-tertiary-hovered)
}
div.better-upload-settings-header > label > input:focus {
border-color: var(--tempo-colors-border-neutral-primary-focused);
}
div.better-upload-progress-bar {
width: 0px;
height: 5px;
background: var(--tempo-colors-background-accent-primary-default);
transition: width 0.5s ease;
border-radius: 10px;
font-size: 14px;
align-items: center;
}
div.better-upload-info-container {
display: flex;
flex-direction: column;
border-bottom: 2px solid var(--tempo-colors-text-neutral-secondary-default);
padding-bottom: 25px;
margin-top: 15px;
}
div.better-upload-info-header {
height: 48px;
display: grid;
grid-template-columns: ${grid_template_columns};
column-gap: 10px;
align-items: center;
padding-left: 8px;
color: var(--tempo-colors-text-neutral-secondary-default);
background: inherit;
border-bottom: 1px solid var(--tempo-colors-divider-neutral-primary-default);
overflow: auto;
scrollbar-gutter: stable;
}
ul.better-upload-info-list {
max-height: 500px;
background: inherit;
margin-top: 10px;
overflow: auto;
scrollbar-gutter: stable;
}
li.better-upload-info-item {
display: grid;
grid-template-columns: ${grid_template_columns};
column-gap: 10px;
height: 56px;
align-items: center;
border-radius: 2px;
font-size: 14px;
padding-left: 8px;
}
li.better-upload-info-item:hover {
background: var(--tempo-colors-background-neutral-secondary-default);
}
li.better-upload-info-item span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--tempo-colors-text-neutral-primary-default);
width: fit-content;
}
li.better-upload-info-item.better-upload-already-uploaded span {
color: var(--tempo-colors-text-neutral-secondary-default);
}
li.better-upload-info-item.better-upload-error span {
color: var(--tempo-colors-text-feedback-error-pressed);
}
li.better-upload-info-item > span.better-upload-status-icon.better-upload-waiting {
animation: spin180 1.2s ease-in-out infinite;
}
li.better-upload-info-item > span.better-upload-status-icon.better-upload-uploading {
animation: brightnesspulse 2s infinite;
}
@keyframes brightnesspulse {
0% { filter: brightness(1); }
50% { filter: brightness(0.5); }
100% { filter: brightness(1); }
}
li.better-upload-info-item > div.better-upload-name-container {
display: flex;
align-items: center;
gap: 12px;
overflow: hidden;
}
li.better-upload-info-item img.better-upload-file-image {
width: 40px;
height: 40px;
border-radius: var(--tempo-radii-2xs);
}
div.better-upload-status {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 10px;
background: var(--tempo-colors-background-neutral-secondary-default);
border-radius: 5px;
}
div.better-upload-status > span {
color: var(--tempo-colors-text-neutral-primary-default);
font-weight: 500;
}
.better-upload-action-button {
padding: 8px;
background: var(--tempo-colors-background-accent-primary-default);
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.better-upload-action-button:hover {
background: var(--tempo-colors-background-accent-primary-hovered);
}
`;
const style = document.createElement("style");
style.type = "text/css";
style.textContent = css;
document.querySelector("head").appendChild(style);
}
}
class Config {
static CONFIG_PATH = "better_upload_config";
CURRENT_CONFIG_VERSION = -1; // needs to be -1 for the very first version
StringConfig = class {
// functions to traverse and edit a json based on string paths
static get_value(obj, path) {
return path.split(".").reduce((acc, key) => acc && acc[key], obj);
}
static set_key(obj, path, value) {
let current = obj;
const keys = path.split(".");
keys.slice(0, -1).forEach(key => {
current[key] = current[key] ?? (/^\d+$/.test(key) ? [] : {});
current = current[key];
});
current[keys[keys.length - 1]] = value;
}
static delete_key(obj, path) {
let current = obj;
const keys = path.split(".");
keys.slice(0, -1).forEach(key => {
if (!current[key]) return;
current = current[key];
});
delete current[keys[keys.length - 1]];
}
static move_key(obj, from, to) {
const value = this.get_value(obj, from);
if (value !== undefined) {
this.set_key(obj, to, value);
this.delete_key(obj, from);
}
}
}
constructor() {
this.config = this.setter_proxy(this.get());
}
retrieve() {
return JSON.parse(localStorage.getItem(Config.CONFIG_PATH)) || {
config_version: this.CURRENT_CONFIG_VERSION,
batch_size: 1,
timeout: 60
}
}
get() {
const config = this.retrieve();
if (config.config_version !== this.CURRENT_CONFIG_VERSION) {
return this.migrate_config(config);
}
return config;
}
save() {
localStorage.setItem(Config.CONFIG_PATH, JSON.stringify(this.config));
}
static static_save(config) {
localStorage.setItem(Config.CONFIG_PATH, JSON.stringify(config));
}
setter_proxy(obj) {
return new Proxy(obj, {
set: (target, key, value) => {
target[key] = value;
this.save();
return true;
},
get: (target, key) => {
if (typeof target[key] === 'object' && target[key] !== null) {
return this.setter_proxy(target[key]); // Ensure nested objects are also proxied
}
return target[key];
}
});
}
migrate_config(config) {
// patch structure
// [from, to, ?value]
// if both "from" and "to" exist, we change the path from "from" to "to"
// if "from" is null, "value" is required as we create/update the key and set the value to "value"
// if "to" is null, we delete the key
const patches = [
]
const old_cfg_version = config.config_version === undefined ? -1 : config.config_version;
for (let patch = old_cfg_version+1; patch <= this.CURRENT_CONFIG_VERSION; patch++) {
if (patch !== 0) { // we add the config_version key in the first patch
config.config_version++;
}
patches[patch].forEach(([from, to, value]) => {
if (from && to) {
this.StringConfig.move_key(config, from, to);
}
else if (!from && to) {
this.StringConfig.set_key(config, to, value);
}
else if (from && !to) {
this.StringConfig.delete_key(config, from);
}
});
logger.console.debug("Migrated to version", patch);
}
logger.console.log("Migrated config to version", this.CURRENT_CONFIG_VERSION);
return config;
}
}
const logger = new Logger();
const config = new Config().config;
const deezer = new Deezer();
(async function main() {
UI.ensure_ui();
})();
})();