您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances the process of uploading mp3 files to Deezer by providing a better UI and handling for uploads.
// ==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(); })(); })();