diff --git a/SpotiFLAC.py b/SpotiFLAC.py
index 556fb6d..08a7725 100644
--- a/SpotiFLAC.py
+++ b/SpotiFLAC.py
@@ -13,15 +13,17 @@ from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit,
QLabel, QFileDialog, QListWidget, QTextEdit, QTabWidget, QButtonGroup, QRadioButton,
QAbstractItemView, QProgressBar, QCheckBox, QDialog,
- QDialogButtonBox, QComboBox, QStyledItemDelegate
+ QDialogButtonBox, QComboBox, QStyledItemDelegate, QMessageBox
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer, QTime, QSettings, QSize
-from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush
+from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush, QPainter, QColor
+from PyQt6.QtSvg import QSvgRenderer
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException
from tidalDL import TidalDownloader
from deezerDL import DeezerDownloader
+from getSecret import scrape_and_save
@dataclass
class Track:
@@ -35,6 +37,25 @@ class Track:
isrc: str = ""
release_date: str = ""
+class SecretScrapeWorker(QThread):
+ finished = pyqtSignal(bool, str)
+ progress = pyqtSignal(str)
+
+ def run(self):
+ try:
+ self.progress.emit("Fixing error...")
+ self.progress.emit("Please wait, this may take a moment...")
+
+ success, message = scrape_and_save(progress_callback=self.progress.emit)
+
+ if success:
+ self.finished.emit(True, "Fixed successfully!")
+ else:
+ self.finished.emit(False, message)
+
+ except Exception as e:
+ self.finished.emit(False, f"Error: {str(e)}")
+
class MetadataFetchWorker(QThread):
finished = pyqtSignal(dict)
error = pyqtSignal(str)
@@ -392,8 +413,8 @@ class ServiceComboBox(QComboBox):
current_dir = os.path.dirname(os.path.abspath(__file__))
self.services = [
- {'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png', 'online': False},
- {'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False}
+ {'id': 'tidal', 'name': 'Tidal', 'icon': 'icons/tidal.png', 'online': False},
+ {'id': 'deezer', 'name': 'Deezer', 'icon': 'icons/deezer.png', 'online': False}
]
for service in self.services:
@@ -455,7 +476,7 @@ class ServiceComboBox(QComboBox):
class SpotiFLACGUI(QWidget):
def __init__(self):
super().__init__()
- self.current_version = "4.8"
+ self.current_version = "4.9"
self.tracks = []
self.all_tracks = []
self.successful_downloads = []
@@ -530,6 +551,7 @@ class SpotiFLACGUI(QWidget):
def reset_ui(self):
self.track_list.clear()
+ self.track_list.show()
self.log_output.clear()
self.progress_bar.setValue(0)
self.progress_bar.hide()
@@ -542,13 +564,33 @@ class SpotiFLACGUI(QWidget):
self.search_input.clear()
if hasattr(self, 'search_widget'):
self.search_widget.hide()
+
+ def get_themed_icon(self, icon_name):
+ icon_path = os.path.join(os.path.dirname(__file__), "icons", icon_name)
+ if not os.path.exists(icon_path):
+ return QIcon()
+
+ with open(icon_path, 'r') as f:
+ svg_content = f.read()
+
+ svg_content = svg_content.replace('currentColor', self.current_theme_color)
+
+ renderer = QSvgRenderer(svg_content.encode())
+ pixmap = QPixmap(16, 16)
+ pixmap.fill(QColor(0, 0, 0, 0))
+
+ painter = QPainter(pixmap)
+ renderer.render(painter)
+ painter.end()
+
+ return QIcon(pixmap)
def initUI(self):
self.setWindowTitle('SpotiFLAC')
self.setFixedWidth(650)
self.setMinimumHeight(350)
- icon_path = os.path.join(os.path.dirname(__file__), "icon.svg")
+ icon_path = os.path.join(os.path.dirname(__file__), "icons", "icon.svg")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
@@ -766,42 +808,42 @@ class SpotiFLACGUI(QWidget):
def setup_track_buttons(self):
self.btn_layout = QHBoxLayout()
- self.download_selected_btn = QPushButton('Download Selected')
- self.download_all_btn = QPushButton('Download All')
- self.remove_btn = QPushButton('Remove Selected')
- self.clear_btn = QPushButton('Clear')
+ self.download_btn = QPushButton(' Download')
+ self.download_btn.setIcon(self.get_themed_icon('download.svg'))
+ self.delete_btn = QPushButton(' Delete')
+ self.delete_btn.setIcon(self.get_themed_icon('trash.svg'))
- for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]:
- btn.setMinimumWidth(120)
+ for btn in [self.download_btn, self.delete_btn]:
+ btn.setFixedWidth(120)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
- self.download_selected_btn.clicked.connect(self.download_selected)
- self.download_all_btn.clicked.connect(self.download_all)
- self.remove_btn.clicked.connect(self.remove_selected_tracks)
- self.clear_btn.clicked.connect(self.clear_tracks)
+ self.download_btn.clicked.connect(self.download_tracks_action)
+ self.delete_btn.clicked.connect(self.delete_tracks)
self.btn_layout.addStretch()
- for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]:
- self.btn_layout.addWidget(btn, 1)
+ self.btn_layout.addWidget(self.download_btn)
+ self.btn_layout.addWidget(self.delete_btn)
self.btn_layout.addStretch()
self.single_track_container = QWidget()
single_track_layout = QHBoxLayout(self.single_track_container)
single_track_layout.setContentsMargins(0, 0, 0, 0)
- self.single_download_btn = QPushButton('Download')
- self.single_clear_btn = QPushButton('Clear')
+ self.single_download_btn = QPushButton(' Download')
+ self.single_download_btn.setIcon(self.get_themed_icon('download.svg'))
+ self.single_delete_btn = QPushButton(' Delete')
+ self.single_delete_btn.setIcon(self.get_themed_icon('trash.svg'))
- for btn in [self.single_download_btn, self.single_clear_btn]:
+ for btn in [self.single_download_btn, self.single_delete_btn]:
btn.setFixedWidth(120)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
- self.single_download_btn.clicked.connect(self.download_all)
- self.single_clear_btn.clicked.connect(self.clear_tracks)
+ self.single_download_btn.clicked.connect(self.download_tracks_action)
+ self.single_delete_btn.clicked.connect(self.delete_tracks)
single_track_layout.addStretch()
single_track_layout.addWidget(self.single_download_btn)
- single_track_layout.addWidget(self.single_clear_btn)
+ single_track_layout.addWidget(self.single_delete_btn)
single_track_layout.addStretch()
self.single_track_container.hide()
@@ -815,6 +857,18 @@ class SpotiFLACGUI(QWidget):
self.log_output.setReadOnly(True)
process_layout.addWidget(self.log_output)
+ fix_error_layout = QHBoxLayout()
+ fix_error_layout.addStretch()
+ self.fix_error_btn = QPushButton(' Fix Error')
+ self.fix_error_btn.setIcon(self.get_themed_icon('tool.svg'))
+ self.fix_error_btn.setFixedWidth(120)
+ self.fix_error_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.fix_error_btn.clicked.connect(self.fix_error_action)
+ self.fix_error_btn.hide()
+ fix_error_layout.addWidget(self.fix_error_btn)
+ fix_error_layout.addStretch()
+ process_layout.addLayout(fix_error_layout)
+
progress_time_layout = QVBoxLayout()
progress_time_layout.setSpacing(2)
@@ -840,7 +894,8 @@ class SpotiFLACGUI(QWidget):
self.stop_btn.clicked.connect(self.stop_download)
self.pause_resume_btn.clicked.connect(self.toggle_pause_resume)
- self.remove_successful_btn = QPushButton('Remove Finished Songs')
+ self.remove_successful_btn = QPushButton(' Remove Finished Tracks')
+ self.remove_successful_btn.setIcon(self.get_themed_icon('circle-x.svg'))
self.remove_successful_btn.setFixedWidth(200)
self.remove_successful_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.remove_successful_btn.clicked.connect(self.remove_successful_downloads)
@@ -1214,6 +1269,25 @@ class SpotiFLACGUI(QWidget):
}
)
+ self.refresh_button_icons()
+
+ def refresh_button_icons(self):
+ if hasattr(self, 'download_btn'):
+ self.download_btn.setIcon(self.get_themed_icon('download.svg'))
+ if hasattr(self, 'delete_btn'):
+ self.delete_btn.setIcon(self.get_themed_icon('trash.svg'))
+
+ if hasattr(self, 'single_download_btn'):
+ self.single_download_btn.setIcon(self.get_themed_icon('download.svg'))
+ if hasattr(self, 'single_delete_btn'):
+ self.single_delete_btn.setIcon(self.get_themed_icon('trash.svg'))
+
+ if hasattr(self, 'fix_error_btn'):
+ self.fix_error_btn.setIcon(self.get_themed_icon('tool.svg'))
+
+ if hasattr(self, 'remove_successful_btn'):
+ self.remove_successful_btn.setIcon(self.get_themed_icon('circle-x.svg'))
+
def setup_about_tab(self):
about_tab = QWidget()
about_layout = QVBoxLayout()
@@ -1319,6 +1393,9 @@ class SpotiFLACGUI(QWidget):
return
try:
+ if hasattr(self, 'fix_error_btn') and self.fix_error_btn.isVisible():
+ self.fix_error_btn.hide()
+
self.reset_state()
self.reset_ui()
@@ -1355,6 +1432,39 @@ class SpotiFLACGUI(QWidget):
def on_metadata_error(self, error_message):
self.log_output.append(f'Error: {error_message}')
+
+ if "Failed to get raw data" in error_message or "Failed to fetch secrets" in error_message or "Failed to get access token" in error_message:
+ if not hasattr(self, 'fix_error_btn') or not self.fix_error_btn.isVisible():
+ self.show_fix_error_button()
+
+ def show_fix_error_button(self):
+ if hasattr(self, 'fix_error_btn'):
+ self.fix_error_btn.show()
+
+ def fix_error_action(self):
+ self.fix_error_btn.setEnabled(False)
+ self.fix_error_btn.setText("Fixing...")
+
+ self.scrape_worker = SecretScrapeWorker()
+ self.scrape_worker.progress.connect(lambda msg: self.log_output.append(msg))
+ self.scrape_worker.finished.connect(self.on_scrape_finished)
+ self.scrape_worker.start()
+
+ def on_scrape_finished(self, success, message):
+ self.log_output.append(message)
+
+ if hasattr(self, 'fix_error_btn'):
+ self.fix_error_btn.setEnabled(True)
+ self.fix_error_btn.setText("Fix Error")
+
+ if success:
+ self.fix_error_btn.hide()
+
+ if success:
+ url = self.spotify_url.text().strip()
+ if url:
+ self.log_output.append("Retrying fetch...")
+ QTimer.singleShot(1000, self.fetch_tracks)
def handle_track_metadata(self, track_data):
track_id = track_data["external_urls"].split("/")[-1]
@@ -1604,37 +1714,27 @@ class SpotiFLACGUI(QWidget):
def update_button_states(self):
if self.is_single_track:
- for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]:
+ for btn in [self.download_btn, self.delete_btn]:
btn.hide()
self.single_track_container.show()
self.single_download_btn.setEnabled(True)
- self.single_clear_btn.setEnabled(True)
+ self.single_delete_btn.setEnabled(True)
else:
self.single_track_container.hide()
- self.download_selected_btn.show()
- self.download_all_btn.show()
- self.remove_btn.show()
- self.clear_btn.show()
+ self.download_btn.show()
+ self.delete_btn.show()
- self.download_all_btn.setText('Download All')
- self.clear_btn.setText('Clear')
-
- self.download_all_btn.setMinimumWidth(120)
- self.clear_btn.setMinimumWidth(120)
-
- self.download_selected_btn.setEnabled(True)
- self.download_all_btn.setEnabled(True)
+ self.download_btn.setEnabled(True)
+ self.delete_btn.setEnabled(True)
def hide_track_buttons(self):
buttons = [
- self.download_selected_btn,
- self.download_all_btn,
- self.remove_btn,
- self.clear_btn
+ self.download_btn,
+ self.delete_btn
]
for btn in buttons:
btn.hide()
@@ -1642,24 +1742,28 @@ class SpotiFLACGUI(QWidget):
if hasattr(self, 'single_track_container'):
self.single_track_container.hide()
- def download_selected(self):
+ def download_tracks_action(self):
if self.is_single_track:
- self.download_all()
+ self.start_download([0])
else:
- selected_items = self.track_list.selectedItems()
+ selected_items = self.track_list.selectedItems()
+
if not selected_items:
- self.log_output.append('Warning: Please select tracks to download.')
- return
- selected_indices = [self.track_list.row(item) for item in selected_items]
- self.download_tracks(selected_indices)
-
- def download_all(self):
- if self.is_single_track:
- self.download_tracks([0])
- else:
- self.download_tracks(range(len(self.tracks)))
-
- def download_tracks(self, indices):
+ reply = QMessageBox.question(
+ self,
+ 'Confirm Download All',
+ f'No tracks selected. Download all {len(self.tracks)} tracks?',
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No
+ )
+
+ if reply == QMessageBox.StandardButton.Yes:
+ self.start_download(range(len(self.tracks)))
+ else:
+ selected_indices = [self.track_list.row(item) for item in selected_items]
+ self.start_download(selected_indices)
+
+ def start_download(self, indices):
self.log_output.clear()
raw_outpath = self.output_dir.text().strip()
outpath = os.path.normpath(raw_outpath)
@@ -1703,13 +1807,12 @@ class SpotiFLACGUI(QWidget):
self.update_ui_for_download_start()
def update_ui_for_download_start(self):
- self.download_selected_btn.setEnabled(False)
- self.download_all_btn.setEnabled(False)
+ self.download_btn.setEnabled(False)
if hasattr(self, 'single_download_btn'):
self.single_download_btn.setEnabled(False)
- if hasattr(self, 'single_clear_btn'):
- self.single_clear_btn.setEnabled(False)
+ if hasattr(self, 'single_delete_btn'):
+ self.single_delete_btn.setEnabled(False)
self.stop_btn.show()
self.pause_resume_btn.show()
@@ -1747,13 +1850,12 @@ class SpotiFLACGUI(QWidget):
else:
self.remove_successful_btn.hide()
- self.download_selected_btn.setEnabled(True)
- self.download_all_btn.setEnabled(True)
+ self.download_btn.setEnabled(True)
if hasattr(self, 'single_download_btn'):
self.single_download_btn.setEnabled(True)
- if hasattr(self, 'single_clear_btn'):
- self.single_clear_btn.setEnabled(True)
+ if hasattr(self, 'single_delete_btn'):
+ self.single_delete_btn.setEnabled(True)
if success:
self.log_output.append(f"\nStatus: {message}")
@@ -1830,26 +1932,36 @@ class SpotiFLACGUI(QWidget):
self.remove_successful_btn.hide()
- def remove_selected_tracks(self):
- if not self.is_single_track:
+ def delete_tracks(self):
+ if self.is_single_track:
+ self.reset_state()
+ self.reset_ui()
+ else:
selected_items = self.track_list.selectedItems()
- selected_indices = [self.track_list.row(item) for item in selected_items]
- tracks_to_remove = [self.tracks[i] for i in selected_indices]
-
- for track in tracks_to_remove:
- if track in self.tracks:
- self.tracks.remove(track)
- if track in self.all_tracks:
- self.all_tracks.remove(track)
-
-
-
- self.update_track_list_display()
-
- def clear_tracks(self):
- self.reset_state()
- self.reset_ui()
+ if not selected_items:
+ reply = QMessageBox.question(
+ self,
+ 'Confirm Delete All',
+ f'No tracks selected. Delete all {len(self.tracks)} tracks?',
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No
+ )
+
+ if reply == QMessageBox.StandardButton.Yes:
+ self.reset_state()
+ self.reset_ui()
+ else:
+ selected_indices = [self.track_list.row(item) for item in selected_items]
+ tracks_to_remove = [self.tracks[i] for i in selected_indices]
+
+ for track in tracks_to_remove:
+ if track in self.tracks:
+ self.tracks.remove(track)
+ if track in self.all_tracks:
+ self.all_tracks.remove(track)
+
+ self.update_track_list_display()
self.tab_widget.setCurrentIndex(0)
def start_timer(self):
diff --git a/getMetadata.py b/getMetadata.py
index c9ff223..3132535 100644
--- a/getMetadata.py
+++ b/getMetadata.py
@@ -1,5 +1,6 @@
from time import sleep
from urllib.parse import urlparse, parse_qs
+from pathlib import Path
import requests
import json
import time
@@ -14,19 +15,32 @@ def get_random_user_agent():
# https://github.com/xyloflake/spot-secrets-go
def generate_totp():
- url = "https://raw.githubusercontent.com/afkarxyz/secretBytes/refs/heads/main/secrets/secretBytes.json"
+ local_path = Path.home() / ".spotify-secret" / "secretBytes.json"
+ used_local = False
try:
+ url = "https://raw.githubusercontent.com/afkarxyz/secretBytes/refs/heads/main/secrets/secretBytes.json"
resp = requests.get(url, timeout=10)
if resp.status_code != 200:
- raise Exception(f"Failed to fetch TOTP secrets from GitHub. Status: {resp.status_code}")
+ raise Exception(f"GitHub fetch failed with status: {resp.status_code}")
secrets_list = resp.json()
-
+ except Exception as github_error:
+ try:
+ if local_path.exists():
+ with open(local_path, 'r') as f:
+ secrets_list = json.load(f)
+ used_local = True
+ else:
+ raise Exception(f"GitHub failed ({github_error}) and no local file found at {local_path}")
+ except Exception as local_error:
+ raise Exception(f"Failed to fetch secrets from both GitHub and local: {local_error}")
+
+ try:
latest_entry = max(secrets_list, key=lambda x: x["version"])
version = latest_entry["version"]
secret_cipher = latest_entry["secret"]
except Exception as e:
- raise Exception(f"Failed to fetch secrets from GitHub: {str(e)}")
+ raise Exception(f"Failed to process secrets: {str(e)}")
processed = [byte ^ ((i % 33) + 9) for i, byte in enumerate(secret_cipher)]
processed_str = "".join(map(str, processed))
diff --git a/getSecret.py b/getSecret.py
new file mode 100644
index 0000000..35a3d36
--- /dev/null
+++ b/getSecret.py
@@ -0,0 +1,111 @@
+import json
+import time
+from pathlib import Path
+from DrissionPage import ChromiumPage, ChromiumOptions
+
+def summarise(caps):
+ real = {}
+ for cap in caps:
+ sec = cap.get("secret")
+ if not sec or not isinstance(sec, str):
+ continue
+ ver = cap.get("version") or cap.get("obj", {}).get("version")
+ if ver and ver != 0:
+ real[str(int(ver))] = sec
+
+ if not real:
+ return False, "No secrets found."
+
+ versions = sorted(int(k) for k in real.keys())
+ secret_bytes = [
+ {"version": v, "secret": [ord(c) for c in real[str(v)]]}
+ for v in versions
+ ]
+
+ secrets_dir = Path.home() / ".spotify-secret"
+ secrets_dir.mkdir(exist_ok=True)
+
+ output_file = secrets_dir / "secretBytes.json"
+ with open(output_file, "w") as f:
+ json.dump(secret_bytes, f, indent=2)
+
+ return True, f"Saved to: {output_file}"
+
+
+def grab_live(progress_callback=None):
+ """
+ Grab secrets from Spotify web player
+ Args:
+ progress_callback: Optional callback function to report progress
+ Returns:
+ list: Captured secrets
+ """
+ def emit_progress(msg):
+ if progress_callback:
+ progress_callback(msg)
+ else:
+ print(msg)
+
+ stealth = """(()=>{
+ Object.defineProperty(navigator,'webdriver',{get:()=>false});
+ Object.defineProperty(navigator,'languages',{get:()=>['en-US','en']});
+ Object.defineProperty(navigator,'plugins',{get:()=>[1,2,3,4,5]});
+ window.chrome={runtime:{}};
+ const q=navigator.permissions.query;
+ navigator.permissions.query=p=>p.name==='notifications'?Promise.resolve({state:Notification.permission}):q(p);
+ const g=WebGLRenderingContext.prototype.getParameter;
+ WebGLRenderingContext.prototype.getParameter=function(p){
+ if(p===37445)return'Intel Inc.';if(p===37446)return'Intel Iris OpenGL Engine';return g.call(this,p);
+ };
+ })();"""
+
+ hook = """(()=>{if(globalThis.__secretHookInstalled)return;
+ globalThis.__secretHookInstalled=true;globalThis.__captures=[];
+ Object.defineProperty(Object.prototype,'secret',{configurable:true,set:function(v){
+ try{__captures.push({secret:v,version:this.version,obj:this});}catch(e){}
+ Object.defineProperty(this,'secret',{value:v,writable:true,configurable:true,enumerable:true});}});
+ })();"""
+
+ co = ChromiumOptions()
+ co.headless(True)
+ co.set_argument('--disable-blink-features=AutomationControlled')
+ co.set_argument('--no-sandbox')
+
+ page = ChromiumPage(addr_or_opts=co)
+ try:
+ page.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=stealth)
+ page.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=hook)
+ emit_progress("Opening Spotify...")
+ page.get("https://open.spotify.com")
+ time.sleep(3)
+ caps = page.run_js("return globalThis.__captures || []")
+ for c in caps:
+ if isinstance(c, dict) and c.get("secret") and c.get("version"):
+ emit_progress(f"Secret({int(c['version'])}): {c['secret']}")
+ return caps or []
+ finally:
+ page.quit()
+
+def scrape_and_save(progress_callback=None):
+ """
+ Main function to scrape secrets and save to file
+ Args:
+ progress_callback: Optional callback function to report progress
+ Returns:
+ tuple: (success: bool, message: str)
+ """
+ try:
+ caps = grab_live(progress_callback)
+ return summarise(caps)
+ except Exception as e:
+ return False, f"Error: {str(e)}"
+
+
+def main():
+ success, message = scrape_and_save()
+ print(message)
+ return 0 if success else 1
+
+if __name__ == "__main__":
+ import sys
+ sys.exit(main())
\ No newline at end of file
diff --git a/icons/circle-x.svg b/icons/circle-x.svg
new file mode 100644
index 0000000..3625f95
--- /dev/null
+++ b/icons/circle-x.svg
@@ -0,0 +1,5 @@
+
diff --git a/deezer.png b/icons/deezer.png
similarity index 100%
rename from deezer.png
rename to icons/deezer.png
diff --git a/icons/download.svg b/icons/download.svg
new file mode 100644
index 0000000..c77f62c
--- /dev/null
+++ b/icons/download.svg
@@ -0,0 +1,5 @@
+
diff --git a/icon.ico b/icons/icon.ico
similarity index 100%
rename from icon.ico
rename to icons/icon.ico
diff --git a/icon.svg b/icons/icon.svg
similarity index 100%
rename from icon.svg
rename to icons/icon.svg
diff --git a/tidal.png b/icons/tidal.png
similarity index 100%
rename from tidal.png
rename to icons/tidal.png
diff --git a/icons/tool.svg b/icons/tool.svg
new file mode 100644
index 0000000..29229f9
--- /dev/null
+++ b/icons/tool.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/trash.svg b/icons/trash.svg
new file mode 100644
index 0000000..20fb5cb
--- /dev/null
+++ b/icons/trash.svg
@@ -0,0 +1,6 @@
+
diff --git a/requirements.txt b/requirements.txt
index 2ae380a..4dcf0fc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,4 +5,5 @@ requests
mutagen
pyotp
packaging
-pyinstaller
\ No newline at end of file
+pyinstaller
+DrissionPage
\ No newline at end of file