Upload
This commit is contained in:
commit
a08c62a976
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
thumbnails
|
||||
scan.json
|
||||
config.ini
|
33
__main__.py
Normal file
33
__main__.py
Normal file
@ -0,0 +1,33 @@
|
||||
import pip, sys, os
|
||||
|
||||
# check if PyQt6 is installed
|
||||
print('### Checking install of PyQt6 ###')
|
||||
try:
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
print("### Check passed ###")
|
||||
except ImportError:
|
||||
print("PyQt6 is not installed. Installing PyQt6...")
|
||||
pip.main(['install', 'PyQt6'])
|
||||
print("### PyQt6 installed ###")
|
||||
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
import traceback, configparser
|
||||
|
||||
from main_window import MainWindow
|
||||
|
||||
def folderCheck(name):
|
||||
if not os.path.exists(name):
|
||||
os.makedirs(name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
folderCheck("thumbnails")
|
||||
|
||||
# configparser
|
||||
try:
|
||||
app = QApplication([])
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
app.exec()
|
||||
except Exception as e:
|
||||
print("### Init Error ###")
|
||||
traceback.print_exc()
|
69
action_runner.py
Normal file
69
action_runner.py
Normal file
@ -0,0 +1,69 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QLabel, QFrame,
|
||||
QVBoxLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
import subprocess, traceback, threading
|
||||
|
||||
class RunningDialog(QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.hide()
|
||||
self.setObjectName("runningdialog")
|
||||
self.setWindowTitle("Running")
|
||||
# always on top
|
||||
self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
|
||||
|
||||
self.setFixedSize(600, 1)
|
||||
# disable resizing
|
||||
self.setFixedSize(self.size())
|
||||
# disable close button
|
||||
self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False)
|
||||
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
|
||||
|
||||
class ActionRunner:
|
||||
def __init__(self, command: str, args: list[str] = []):
|
||||
self.command = command
|
||||
self.args = args
|
||||
|
||||
def run(self):
|
||||
running_dialog = RunningDialog(None)
|
||||
running_dialog.show()
|
||||
running_dialog.setWindowTitle("Running: " + self.command + " " + " ".join(self.args) )
|
||||
try:
|
||||
process=subprocess.Popen(
|
||||
" ".join([self.command] + self.args),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
output = process.stdout.readlines()
|
||||
err = process.stderr.readlines()
|
||||
|
||||
if output == []:
|
||||
print("Output:", output)
|
||||
print("err:")
|
||||
for e in err:
|
||||
print(e.decode("utf-8"))
|
||||
raise Exception("Error while invoking command: " + self.command + " " + " ".join(self.args))
|
||||
|
||||
lines = []
|
||||
for line in output:
|
||||
l = line.decode("utf-8")
|
||||
lines.append(l)
|
||||
|
||||
result = "".join(lines)
|
||||
|
||||
# remove last newline
|
||||
if result.endswith("\n"):
|
||||
result = result[:-1]
|
||||
|
||||
running_dialog.close()
|
||||
return result
|
||||
except Exception as e:
|
||||
running_dialog.close()
|
||||
print("### ActionRunner Error ###")
|
||||
print(e)
|
||||
traceback.print_exc()
|
||||
return None
|
31
config.py
Normal file
31
config.py
Normal file
@ -0,0 +1,31 @@
|
||||
import json, os
|
||||
|
||||
class configManager():
|
||||
def __init__(self):
|
||||
print("Loading config...")
|
||||
self.config = {
|
||||
"config_version": 1,
|
||||
"music_folder": "C:/Users/username/Music",
|
||||
}
|
||||
|
||||
def setConfigTemplate(self, template):
|
||||
self.config = template
|
||||
|
||||
def load_config(self):
|
||||
# load config
|
||||
if os.path.exists("config.json"):
|
||||
with open("config.json", "r") as config_file:
|
||||
self.config = json.load(config_file)
|
||||
else:
|
||||
self.save_config()
|
||||
|
||||
def save_config(self):
|
||||
# save config
|
||||
with open("config.json", "w") as config_file:
|
||||
json.dump(self.config, config_file)
|
||||
|
||||
def get(self, key):
|
||||
return self.config.get(key, None)
|
||||
|
||||
def set(self, key, value):
|
||||
self.config[key] = value
|
BIN
default.png
Normal file
BIN
default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
112
dialogs.py
Normal file
112
dialogs.py
Normal file
@ -0,0 +1,112 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QLabel, QPushButton,
|
||||
QWidget, QFrame, QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
|
||||
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
|
||||
|
||||
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
|
||||
|
||||
QDialog, QFileDialog
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
class SimpleHeader(QFrame):
|
||||
def __init__(self, parent) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("SimpleHeader")
|
||||
self.setStyleSheet("#SimpleHeader { border: 1px solid #00000000; border-bottom-color: #444; }")
|
||||
|
||||
self.parent = parent
|
||||
self.setFixedHeight(24)
|
||||
self.label = QLabel("Simple Header", self)
|
||||
self.label.move(12, 0)
|
||||
self.label.resize( self.width() - 24, 24)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self.label.resize( self.width() - 24, 24)
|
||||
self.resize(self.parent.width(), 24 )
|
||||
|
||||
def setLabel(self, text):
|
||||
self.label.setText(text)
|
||||
|
||||
class ErrorPopup(QDialog):
|
||||
def __init__(self, parent, message, trace, CanIgnore=True, buttonLayout=0) -> None:
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.trace = trace
|
||||
self.canTerminate = CanIgnore
|
||||
self.setWindowTitle("Error")
|
||||
self.setGeometry(000, 150, 600, 300)
|
||||
# reset styles
|
||||
# self.setStyleSheet("")
|
||||
# self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
|
||||
self.setWindowModality(Qt.WindowModality.WindowModal)
|
||||
|
||||
self.header = SimpleHeader(self)
|
||||
self.header.setLabel(self.message)
|
||||
self.header.move(0, 0)
|
||||
|
||||
# self.label = QLabel(self.message, self)
|
||||
# self.label.move(12, 0)
|
||||
# self.label.resize( self.width() - 24, 24)
|
||||
|
||||
self.trace = QLabel(self.trace, self)
|
||||
self.trace.move(12, 24)
|
||||
self.trace.resize( self.width() - 24, 24)
|
||||
|
||||
match buttonLayout:
|
||||
case 0:
|
||||
pass
|
||||
self.ignore = QPushButton(CanIgnore and "Ignore" or "Critical Error", self)
|
||||
self.ignore.move(12, self.height() - 36)
|
||||
self.ignore.resize( self.width() - 24, 24)
|
||||
self.ignore.clicked.connect(self.close)
|
||||
self.ignore.setDisabled(not CanIgnore)
|
||||
|
||||
self.btn_terminate = QPushButton("Terminate", self)
|
||||
self.btn_terminate.move(12, self.height() - 60)
|
||||
self.btn_terminate.resize( self.width() - 24, 24)
|
||||
self.btn_terminate.clicked.connect(self.terminate)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
# self.move(int(self.parent.width()/2) - int(self.width()/2), int(self.parent.height()/2) - int(self.height()/2))
|
||||
self.header.resize( self.width(), 24)
|
||||
self.trace.resize( self.width() - 24, self.height() - 60)
|
||||
self.ignore.move(12, self.height() - 36)
|
||||
self.ignore.resize( int(self.width()/2) - 24, 24)
|
||||
self.btn_terminate.move(int(self.width()/2) + 12, self.height() - 36)
|
||||
self.btn_terminate.resize( int(self.width()/2) - 24, 24)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
|
||||
def terminate(self):
|
||||
self.parent.close()
|
||||
super().close()
|
||||
exit(1)
|
||||
|
||||
# on window close
|
||||
def closeEvent(self, event):
|
||||
if self.canTerminate:
|
||||
self.close()
|
||||
else:
|
||||
self.terminate()
|
||||
|
||||
class ListDialog(QDialog):
|
||||
def __init__(self, parent = None, dialogHeader = None, header = None, data = None):
|
||||
super(ListDialog, self).__init__(parent)
|
||||
self.setWindowTitle(dialogHeader)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(QLabel(header))
|
||||
# scroll area
|
||||
scroll = QScrollArea()
|
||||
widget = QWidget()
|
||||
widget.setLayout(QVBoxLayout())
|
||||
layout.addWidget(scroll)
|
||||
self.setLayout(layout)
|
||||
for item in data:
|
||||
widget.layout().addWidget(QLabel(item))
|
||||
scroll.setWidget(widget)
|
||||
self.exec()
|
||||
|
26
events.py
Normal file
26
events.py
Normal file
@ -0,0 +1,26 @@
|
||||
class Event():
|
||||
def __init__(self):
|
||||
self.__listeners = []
|
||||
|
||||
@property
|
||||
def on(self, event, callback):
|
||||
self.__listeners.append({event: callback})
|
||||
|
||||
def emit(self, event, data):
|
||||
for listener in self.__listeners:
|
||||
if event in listener:
|
||||
# is this a awaitable?
|
||||
if hasattr(listener[event], "__await__"):
|
||||
listener[event](data)
|
||||
else:
|
||||
listener[event](data)
|
||||
|
||||
def addListener(self, event, callback):
|
||||
if callback in self.__listeners:
|
||||
return
|
||||
self.__listeners.append({event: callback})
|
||||
|
||||
def removeListener(self, event, callback):
|
||||
if callback not in self.__listeners:
|
||||
return
|
||||
self.__listeners.remove({event: callback})
|
176
extractor.py
Normal file
176
extractor.py
Normal file
@ -0,0 +1,176 @@
|
||||
import os, json
|
||||
import traceback
|
||||
|
||||
def escape_file_name(fname):
|
||||
"""
|
||||
Helper function to safely convert the file name (a.k.a. escaping) with
|
||||
spaces which can cause issues when passing over to the command line.
|
||||
"""
|
||||
result = fname.replace('\"', '\\"') # escapes double quotes first
|
||||
result = ['"',result,'"']
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def BytesToReadable(bytes):
|
||||
sizes = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
while bytes > 1024:
|
||||
bytes = bytes / 1024
|
||||
i += 1
|
||||
return f"{bytes:.2f} {sizes[i]}"
|
||||
|
||||
def get_file_size(file_path):
|
||||
"""
|
||||
Returns the size of the file in bytes.
|
||||
"""
|
||||
return os.path.getsize(file_path)
|
||||
|
||||
import subprocess
|
||||
|
||||
def CheckFFPROBE():
|
||||
try:
|
||||
process=subprocess.Popen(
|
||||
" ".join(["ffprobe", "-version"]),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
|
||||
output = process.stdout.readlines()
|
||||
err = process.stderr.readlines()
|
||||
|
||||
if output == []:
|
||||
err_msg = "".join(err)
|
||||
raise Exception("Error while invoking ffprobe:\n" + err_msg)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return
|
||||
def CheckFFMPEG():
|
||||
try:
|
||||
process=subprocess.Popen(
|
||||
" ".join(["ffmpeg", "-version"]),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
|
||||
output = process.stdout.readlines()
|
||||
err = process.stderr.readlines()
|
||||
|
||||
if output == []:
|
||||
err_msg = "".join(err)
|
||||
raise Exception("Error while invoking ffmpeg:\n" + err_msg)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
class ExtractorException(Exception):
|
||||
def __init__(self, errors):
|
||||
self.errors = errors
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.errors)
|
||||
|
||||
def ExtractMetadata(file):
|
||||
print("Extracting Metadata: ", file)
|
||||
|
||||
AdvancedData = {
|
||||
"file:" : file,
|
||||
"data": "Unknown",
|
||||
}
|
||||
|
||||
test = CheckFFPROBE()
|
||||
if not test:
|
||||
print("### FFPROBE: Check failed ###")
|
||||
e = ExtractorException("FFPROBE not found or not installed.")
|
||||
raise e
|
||||
|
||||
try:
|
||||
escaped_fpath = escape_file_name(file)
|
||||
process=subprocess.Popen(
|
||||
" ".join(["ffprobe",
|
||||
"-show_entries format_tags",
|
||||
"-print_format json",
|
||||
escaped_fpath],),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
|
||||
output = process.stdout.readlines()
|
||||
err = process.stderr.readlines()
|
||||
|
||||
if output == []:
|
||||
err_msg = "".join(err)
|
||||
raise Exception("Error while invoking ffprobe: " + err_msg)
|
||||
|
||||
ffprobe_json = []
|
||||
skip_line = True
|
||||
# cleanse any additional information that is not valid JSON
|
||||
for line in output:
|
||||
# string out b' and \r\n
|
||||
line = line.decode("utf-8")
|
||||
if line.strip() == '{':
|
||||
skip_line = False
|
||||
if not skip_line:
|
||||
ffprobe_json.append(line)
|
||||
|
||||
result = json.loads("".join(ffprobe_json))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print("### Extractor Error ###")
|
||||
print(e)
|
||||
except:
|
||||
pass
|
||||
|
||||
return AdvancedData
|
||||
|
||||
def ExtractMediaData(file, output_file):
|
||||
"""
|
||||
Extracts media data from a file using ffmpeg.
|
||||
"""
|
||||
|
||||
try:
|
||||
escaped_fpath = escape_file_name(file)
|
||||
# get the absolute path to the output file
|
||||
current_dir = os.getcwd()
|
||||
absout = os.path.join(current_dir, output_file)
|
||||
|
||||
if os.path.exists(absout):
|
||||
return absout
|
||||
|
||||
print("Extracting Media Data: ", file)
|
||||
|
||||
subprocess.Popen(
|
||||
" ".join(["ffmpeg",
|
||||
"-i",
|
||||
escaped_fpath,
|
||||
"-an",
|
||||
"-vcodec",
|
||||
"copy",
|
||||
f'"{absout}"'],), # output file should be in quotes to avoid spaces in the path 'E:\Downloads\4kvideodownloader\video.mp4
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
return absout
|
||||
except Exception as e:
|
||||
print("### Extractor Error ###")
|
||||
traceback.print_exc()
|
||||
except:
|
||||
pass
|
33
main_window.py
Normal file
33
main_window.py
Normal file
@ -0,0 +1,33 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QLabel, QVBoxLayout,
|
||||
QWidget, QFrame, QSplitter, QStackedWidget, QTabWidget
|
||||
)
|
||||
|
||||
from tab_musiclibrary import MusicLibraryTab
|
||||
from tab_settings import SettingsTab
|
||||
|
||||
class MusicLibrary(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
library = QTabWidget()
|
||||
library.addTab(MusicLibraryTab(), "Songs")
|
||||
library.addTab(SettingsTab(), "Settings")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(library)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("My App")
|
||||
self.setFixedSize(800, 600)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget( MusicLibrary() )
|
||||
container = QWidget()
|
||||
container.setLayout(layout)
|
||||
container.layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.setCentralWidget(container)
|
||||
|
99
settings_widgets.py
Normal file
99
settings_widgets.py
Normal file
@ -0,0 +1,99 @@
|
||||
import traceback
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QLabel, QPushButton,
|
||||
QWidget, QFrame, QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
|
||||
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
|
||||
|
||||
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
|
||||
|
||||
QDialog, QFileDialog
|
||||
)
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtCore import Qt, QAbstractListModel
|
||||
|
||||
from dialogs import ErrorPopup, ListDialog
|
||||
|
||||
import os, sys, json
|
||||
|
||||
# settings widgets
|
||||
|
||||
class SettingBox(QFrame):
|
||||
def __init__(self, parent, name, description = None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("settingbox")
|
||||
self.name = name
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.label = QLabel(name, self)
|
||||
self.label.setObjectName("settinglabel")
|
||||
self.layout.addWidget(self.label)
|
||||
|
||||
if description:
|
||||
self.description = QLabel(description, self)
|
||||
self.layout.addWidget(self.description)
|
||||
|
||||
def AddSetting(self, setting):
|
||||
self.layout.addWidget(setting)
|
||||
|
||||
class SettingButton(QFrame):
|
||||
def __init__(self, parent, text, description = None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("settingbutton")
|
||||
self.description = None
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.button = QPushButton("Button", self)
|
||||
self.layout.addWidget(self.button)
|
||||
self.button.setFixedWidth(100)
|
||||
|
||||
self.setting_info_base = QWidget()
|
||||
self.setting_info = QVBoxLayout(self.setting_info_base)
|
||||
self.setting_info.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.setting_info_base)
|
||||
|
||||
self.label = QLabel(text, self)
|
||||
self.label.setObjectName("settinglabel")
|
||||
self.setting_info.addWidget(self.label)
|
||||
|
||||
if description:
|
||||
self.description = QLabel(description, self)
|
||||
self.setting_info.addWidget(self.description)
|
||||
|
||||
def SetText(self, text):
|
||||
self.label.setText(text)
|
||||
|
||||
def SetDescription(self, text):
|
||||
# check the type of the description
|
||||
if type(text) == bool:
|
||||
text = str(text)
|
||||
if type(text) == list:
|
||||
text = "\n".join(text)
|
||||
if type(text) == dict:
|
||||
text = json.dumps(text, indent=4)
|
||||
if type(text) == traceback.FrameSummary:
|
||||
text = traceback.format_tb(text)
|
||||
|
||||
if not self.description:
|
||||
self.description = QLabel(text, self)
|
||||
self.setting_info.addWidget(self.description)
|
||||
|
||||
if self.description:
|
||||
self.description.setText(text)
|
||||
|
||||
def GetButton(self):
|
||||
return self.button
|
||||
|
||||
def SetButtonText(self, text):
|
||||
self.button.setText(text)
|
||||
|
||||
def SetButtonPressed(self, func):
|
||||
self.button.clicked.connect(func)
|
||||
|
||||
def SetButtonWidth(self, width):
|
||||
self.button.setFixedWidth(width)
|
301
tab_musiclibrary.py
Normal file
301
tab_musiclibrary.py
Normal file
@ -0,0 +1,301 @@
|
||||
import traceback
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QLabel, QPushButton, QLineEdit,
|
||||
QWidget, QFrame, QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
|
||||
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
|
||||
|
||||
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
|
||||
|
||||
QDialog, QFileDialog
|
||||
)
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtCore import Qt, QAbstractListModel
|
||||
|
||||
from dialogs import ErrorPopup, ListDialog
|
||||
|
||||
import os, sys, json, time
|
||||
from extractor import ExtractMetadata, ExtractMediaData
|
||||
from action_runner import RunningDialog
|
||||
|
||||
def calculateAspectRatio(pixmap, width, height, aspectRatioMode):
|
||||
return pixmap.scaled(width, height, aspectRatioMode)
|
||||
# E:\Downloads\4kvideodownloader
|
||||
|
||||
class FolderScanner:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def scan(self):
|
||||
data = []
|
||||
|
||||
if not os.path.exists(self.path):
|
||||
print("Path does not exist: ", self.path)
|
||||
return data
|
||||
|
||||
for root, dirs, files in os.walk(self.path):
|
||||
for file in files:
|
||||
if file.endswith(".mp3"):
|
||||
p = os.path.join(root, file)
|
||||
metadata = ExtractMetadata(p)
|
||||
# debug.json
|
||||
# with open(f"{file}.debug.json", "w") as f:
|
||||
# json.dump(metadata, f, indent=4)
|
||||
|
||||
data.append(metadata)
|
||||
metadata.update({
|
||||
"file:": p
|
||||
})
|
||||
if "data" not in data:
|
||||
metadata.update({
|
||||
"thumbnail": self.ExtractImageData(p, f"thumbnails\\{file}.png"),
|
||||
})
|
||||
|
||||
return data
|
||||
def ExtractImageData(self, file, output):
|
||||
try:
|
||||
return ExtractMediaData(file, output)
|
||||
except Exception as e:
|
||||
print("### ExtractImageData Error ###")
|
||||
traceback.print_exc()
|
||||
|
||||
pass
|
||||
|
||||
class MusicModel(QAbstractListModel):
|
||||
def __init__(self, data):
|
||||
super().__init__()
|
||||
self._data = data
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self._data)
|
||||
|
||||
def data(self, index, role):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
return self._data[index.row()]
|
||||
|
||||
class MusicModelWidget(QFrame):
|
||||
def __init__(self, parent = None, name = None, path = None):
|
||||
super(MusicModelWidget, self).__init__(parent)
|
||||
self.setObjectName(name)
|
||||
layout = QHBoxLayout()
|
||||
self.image = image( "default.png" )
|
||||
self.image.setFixedSize(64, 64)
|
||||
iteminfo_base = QWidget()
|
||||
iteminfo = QVBoxLayout(iteminfo_base)
|
||||
iteminfo.setSpacing(0)
|
||||
self.itemname = QLabel(name)
|
||||
self.itemname.setObjectName("itemname")
|
||||
self.artist = QLabel("Artist")
|
||||
iteminfo.addWidget(self.itemname)
|
||||
iteminfo.addWidget(self.artist)
|
||||
|
||||
layout.addWidget(self.image)
|
||||
layout.addWidget(iteminfo_base)
|
||||
|
||||
self.setStyleSheet("#itemname { font-size: 16px; }")
|
||||
self.setLayout(layout)
|
||||
|
||||
def openFile(self):
|
||||
os.startfile(self.data["format"]["filename"])
|
||||
|
||||
|
||||
class image(QFrame):
|
||||
def __init__(self, path = None):
|
||||
super().__init__()
|
||||
if path is None:
|
||||
path = "default.png"
|
||||
|
||||
self.setObjectName("Logo")
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
|
||||
self.sceene = QGraphicsScene(self)
|
||||
self.view = QGraphicsView(self.sceene, self)
|
||||
self.view.setFixedSize(self.width(), self.height())
|
||||
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.view.move(0, 0)
|
||||
# remove border
|
||||
self.view.setLineWidth(0)
|
||||
self.view.setMidLineWidth(0)
|
||||
self.view.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.view.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.view.setStyleSheet("background-color: transparent;")
|
||||
|
||||
self.pixmap = None
|
||||
self.pixmap = QGraphicsPixmapItem()
|
||||
self.sceene.addItem(self.pixmap)
|
||||
|
||||
try:
|
||||
self.pixmap.setPixmap(QPixmap(path).scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio))
|
||||
except Exception as e:
|
||||
print("### Init Error ###")
|
||||
traceback.print_exc()
|
||||
|
||||
def loadimage(self, path):
|
||||
try:
|
||||
print("Setting image: ", path)
|
||||
self.pixmap.setPixmap(QPixmap(path).scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio))
|
||||
except Exception as e:
|
||||
print("### Load Image Error ###")
|
||||
traceback.print_exc()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self.view.setFixedSize(self.width(), self.height())
|
||||
calculateAspectRatio(self.pixmap.pixmap(), self.width(), self.height(), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
class MusicItem:
|
||||
def __init__(self):
|
||||
self.data = None
|
||||
self.image = None
|
||||
|
||||
self.title = None
|
||||
self.artist = None
|
||||
self.imagepath = None
|
||||
|
||||
def setupData(self, ffprobeJsonData):
|
||||
self.data = ffprobeJsonData
|
||||
data_format = self.data["format"]
|
||||
# get title from the tags
|
||||
if "tags" in data_format:
|
||||
if "title" in data_format["tags"]:
|
||||
self.title = data_format["tags"]["title"]
|
||||
if "artist" in data_format["tags"]:
|
||||
self.artist = data_format["tags"]["artist"]
|
||||
|
||||
if "thumbnail" in self.data:
|
||||
self.image = self.data["thumbnail"]
|
||||
|
||||
def getImage(self):
|
||||
return self.image
|
||||
|
||||
def openFile(self):
|
||||
try:
|
||||
os.startfile(self.data['file:'])
|
||||
except Exception as e:
|
||||
print("Unable to open file: ", e)
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class MusicLibraryTab(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.list = QListWidget()
|
||||
self.scanpathInput = QLineEdit()
|
||||
self.scanpathInput.setPlaceholderText("Path to scan")
|
||||
self.scanbutton = QPushButton("Scan")
|
||||
self.scanbutton.clicked.connect(self.scan)
|
||||
|
||||
self.list.setObjectName("list")
|
||||
self.list.setLineWidth(2)
|
||||
self.list.setMidLineWidth(2)
|
||||
self.list.setFrameShadow(QFrame.Shadow.Raised)
|
||||
self.list.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
|
||||
self.list.doubleClicked.connect(lambda: self.list.currentItem().data(0).openFile())
|
||||
|
||||
scan_base = QWidget()
|
||||
scan_layout = QHBoxLayout(scan_base)
|
||||
scan_layout.setSpacing(0)
|
||||
scan_layout.setContentsMargins(0, 0, 0, 0)
|
||||
scan_layout.addWidget(self.scanbutton)
|
||||
scan_layout.addWidget(self.scanpathInput)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(scan_base)
|
||||
layout.addWidget(self.list)
|
||||
self.setLayout(layout)
|
||||
|
||||
# check if scan.json exists
|
||||
if os.path.exists("scan.json"):
|
||||
self.scan("scan.json")
|
||||
|
||||
def scan(self, file = None):
|
||||
path = self.scanpathInput.text()
|
||||
musicscanner = FolderScanner(path)
|
||||
runningDialog = RunningDialog(None)
|
||||
|
||||
try:
|
||||
# clear list
|
||||
self.list.clear()
|
||||
runningDialog.show()
|
||||
runningDialog.setWindowTitle("Scanning: " + path)
|
||||
scandate = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
if not file:
|
||||
# formated date time
|
||||
scandate = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
scan = musicscanner.scan()
|
||||
else:
|
||||
loadeddata = json.load(open(file))["scan"]
|
||||
scan = loadeddata
|
||||
|
||||
unknownData = False
|
||||
unknownFiles = []
|
||||
|
||||
# debug.scan.json
|
||||
with open("scan.json", "w") as f:
|
||||
settings={
|
||||
"path": self.scanpathInput.text(),
|
||||
"scanned": scandate
|
||||
}
|
||||
finaldata ={
|
||||
"settings": settings,
|
||||
"scan": scan
|
||||
}
|
||||
json.dump(finaldata, f, indent=4)
|
||||
|
||||
|
||||
# check if any of the data is unknown
|
||||
for data in scan:
|
||||
# does data key exist
|
||||
if "data" in data:
|
||||
# is data key unknown
|
||||
if data["data"] == "Unknown":
|
||||
print("Unknown data: ", data["file:"])
|
||||
unknownData = True
|
||||
unknownFiles.append(data["file:"])
|
||||
|
||||
#
|
||||
music = MusicItem()
|
||||
try:
|
||||
music.setupData(data)
|
||||
except Exception as e:
|
||||
print("Skipping setup data: ", e)
|
||||
item = QListWidgetItem(self.list)
|
||||
item.setData(0, music)
|
||||
item_frame = MusicModelWidget(self.list, music.title)
|
||||
if music.image:
|
||||
runningDialog.setWindowTitle("Loading Image: " + music.image)
|
||||
item_frame.image.loadimage(music.image)
|
||||
item.setSizeHint(item_frame.sizeHint())
|
||||
if music.artist:
|
||||
item_frame.artist.setText(music.artist)
|
||||
else:
|
||||
item_frame.artist.setText("Unknown Artist")
|
||||
|
||||
|
||||
# double click
|
||||
# item_frame.itemname.mouseDoubleClickEvent = os.startfile(data["format"]["filename"])
|
||||
|
||||
self.list.addItem(item)
|
||||
self.list.setItemWidget(item, item_frame)
|
||||
|
||||
if unknownData:
|
||||
ListDialog(None, "Unknown Files", "Some files metadata could not be extracted", unknownFiles)
|
||||
|
||||
runningDialog.close()
|
||||
except Exception as e:
|
||||
runningDialog.close()
|
||||
traceback.print_exc()
|
||||
try:
|
||||
ErrorPopup(self, str(e), str(traceback.format_exc(
|
||||
limit=1, chain=True
|
||||
))).exec()
|
||||
except Exception as e:
|
||||
print("Unable to show error popup: ", e)
|
||||
|
||||
pass
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self.list.resize(self.width(), self.height())
|
231
tab_settings.py
Normal file
231
tab_settings.py
Normal file
@ -0,0 +1,231 @@
|
||||
import traceback, webbrowser
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QPushButton, QWidget, QFrame,
|
||||
QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
|
||||
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
|
||||
|
||||
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
|
||||
)
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtCore import Qt, QAbstractListModel
|
||||
|
||||
from dialogs import ErrorPopup, ListDialog
|
||||
from settings_widgets import SettingBox, SettingButton
|
||||
from extractor import CheckFFPROBE
|
||||
|
||||
import os, sys, json
|
||||
from action_runner import ActionRunner, RunningDialog
|
||||
|
||||
class SidebarPage(QFrame):
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.setObjectName("SidebarPage")
|
||||
self.setStyleSheet("#SidebarPage { border: 1px solid #00000000; border-left-color: #444; }")
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def AddWidget(self, widget) -> None:
|
||||
self.layout.addWidget(widget)
|
||||
|
||||
def AddLayout(self, layout) -> None:
|
||||
self.layout.addLayout(layout)
|
||||
|
||||
def AddSpacer(self) -> None:
|
||||
self.layout.addStretch(1)
|
||||
|
||||
class SidebarButton(QPushButton):
|
||||
def __init__(self, text, parent):
|
||||
super().__init__(text, parent)
|
||||
self.defaultstyle = "border: 1px solid #00000000; padding: 4px 8px; color: #000; text-align: left;"
|
||||
self.setObjectName("SidebarButton")
|
||||
self.setStyleSheet("#SidebarButton { " + self.defaultstyle + " background-color: #00000000; }")
|
||||
|
||||
def SetSelected(self, selected):
|
||||
if selected:
|
||||
self.setStyleSheet("#SidebarButton { " + self.defaultstyle + " background-color: #444; color: #fff; }")
|
||||
else:
|
||||
self.setStyleSheet("#SidebarButton { " + self.defaultstyle + " background-color: #00000000; }")
|
||||
|
||||
class Sidebar(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pages = []
|
||||
|
||||
self.setObjectName("Sidebar")
|
||||
|
||||
self.mainlayout = QHBoxLayout()
|
||||
self.mainlayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.mainlayout)
|
||||
|
||||
self.left_base = QWidget()
|
||||
self.left_base.setFixedWidth(200)
|
||||
self.left = QVBoxLayout(self.left_base)
|
||||
# gap between buttons : 0px
|
||||
self.left.setSpacing(0)
|
||||
|
||||
self.left.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.left.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.right_base = QWidget()
|
||||
self.right = QVBoxLayout(self.right_base)
|
||||
self.right.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.right.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.mainlayout.addWidget(self.left_base)
|
||||
self.mainlayout.addWidget(self.right_base)
|
||||
|
||||
def swisthPage(self, index) -> QFrame:
|
||||
self.showPage(self.pages[index])
|
||||
return self.pages[index]
|
||||
|
||||
def AddButton(self, text) -> QPushButton:
|
||||
button = SidebarButton(text, self)
|
||||
self.left.addWidget(button)
|
||||
return button
|
||||
|
||||
def showPage(self, page) -> None:
|
||||
for p in self.pages:
|
||||
p.hide()
|
||||
|
||||
# get buttons
|
||||
for b in self.left_base.children():
|
||||
if isinstance(b, QPushButton):
|
||||
b.SetSelected(False)
|
||||
if b.text() == page.name:
|
||||
b.SetSelected(True)
|
||||
page.show()
|
||||
|
||||
def AddPage(self, name, page) -> None:
|
||||
# hide all pages
|
||||
button = self.AddButton(name)
|
||||
button.clicked.connect(lambda: self.showPage(page))
|
||||
|
||||
for p in self.pages:
|
||||
p.hide()
|
||||
|
||||
self.right.addWidget(page)
|
||||
self.pages.append(page)
|
||||
|
||||
return page
|
||||
|
||||
class subpage_general(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.setStyleSheet(
|
||||
"#subpage_general_title { font-size: 24px; font-weight: bold; }"
|
||||
)
|
||||
|
||||
header = QLabel("General")
|
||||
header.setObjectName("subpage_general_title")
|
||||
|
||||
self.layout.addWidget(header)
|
||||
self.layout.addWidget(QLabel("General settings for the application."))
|
||||
|
||||
# settings
|
||||
self.layout.addWidget(SettingBox(self, "No general settings available."))
|
||||
|
||||
class subpage_requirements(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.setStyleSheet(
|
||||
"#subpage_requirements_title { font-size: 24px; font-weight: bold; }"
|
||||
)
|
||||
|
||||
header = QLabel("Requirements")
|
||||
header.setObjectName("subpage_requirements_title")
|
||||
|
||||
self.layout.addWidget(header)
|
||||
self.layout.addWidget(QLabel("Important applications and libraries required for the application to function."))
|
||||
|
||||
|
||||
setting_echo_test = SettingButton(self, "Echo Test", "Test the echo command.")
|
||||
setting_update_winget = SettingButton(self, "Manual download and install", "Download and install winget manually.")
|
||||
setting_install_ffprobe = SettingButton(self, "Install FFMPEG", "Install or update FFMPEG ( includes FFPROBE ) for media processing.")
|
||||
self.layout.addWidget(setting_echo_test)
|
||||
self.layout.addWidget(setting_update_winget)
|
||||
self.layout.addWidget(setting_install_ffprobe)
|
||||
|
||||
if not CheckFFPROBE():
|
||||
setting_install_ffprobe.SetText("Required: Install FFMPEG")
|
||||
|
||||
setting_echo_test.SetButtonText("Run action")
|
||||
echo_test = ActionRunner("echo", ["Hello World!"])
|
||||
|
||||
ffprobe_installer = ActionRunner("winget", ["install", "-e", "--id", "Gyan.FFmpeg"])
|
||||
def echo_test_button():
|
||||
run = echo_test.run()
|
||||
setting_echo_test.SetText("Echo Test Complete")
|
||||
setting_echo_test.SetDescription(run)
|
||||
|
||||
|
||||
setting_echo_test.SetButtonPressed(echo_test_button)
|
||||
|
||||
setting_update_winget.SetButtonText("Start download")
|
||||
def update_winget():
|
||||
# https://aka.ms/getwinget
|
||||
webbrowser.open("https://aka.ms/getwinget")
|
||||
|
||||
setting_update_winget.SetButtonPressed(update_winget)
|
||||
|
||||
setting_install_ffprobe.SetButtonText("Run action")
|
||||
def install_ffprobe():
|
||||
installed = CheckFFPROBE()
|
||||
print("Status:", installed)
|
||||
print(installed and "FFPROBE Upadting." or "FFPROBE Installing.")
|
||||
ffprobe_installer.run()
|
||||
print(installed and "FFPROBE Upadted." or "FFPROBE Installed.")
|
||||
setting_install_ffprobe.SetText( installed and "FFPROBE Upadted." or "FFPROBE Installed." )
|
||||
setting_install_ffprobe.SetButtonPressed(install_ffprobe)
|
||||
|
||||
|
||||
class SettingsTab(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setStyleSheet(
|
||||
"#settinglabel { font-size: 16px; }"
|
||||
"#settingbutton, #settingbox { border: 1px solid #8c8c8c; padding: 4px 8px; border-radius: 4px;}"
|
||||
)
|
||||
|
||||
try:
|
||||
self.sidebar = Sidebar()
|
||||
|
||||
# general = self.sidebar.AddButton("General")
|
||||
# general.clicked.connect(lambda: print("General"))
|
||||
# general = self.sidebar.AddButton("Requirements")
|
||||
# general.clicked.connect(lambda: print("Requirements"))
|
||||
|
||||
page_general = SidebarPage( "General" )
|
||||
page_general.AddWidget(subpage_general())
|
||||
page_general.AddSpacer()
|
||||
|
||||
page_requirements = SidebarPage( "Requirements" )
|
||||
page_requirements.AddWidget(subpage_requirements())
|
||||
page_requirements.AddSpacer()
|
||||
|
||||
self.sidebar.AddPage("General", page_general)
|
||||
self.sidebar.AddPage("Requirements", page_requirements)
|
||||
self.sidebar.swisthPage(0)
|
||||
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.layout.addWidget(self.sidebar)
|
||||
self.setLayout(self.layout)
|
||||
self.setObjectName("Settings")
|
||||
except Exception as e:
|
||||
print("### SettingsTab Error ###")
|
||||
traceback.print_exc()
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(QLabel("SettingsTab Error"))
|
||||
self.setLayout(self.layout)
|
Loading…
Reference in New Issue
Block a user