Compare commits

...

2 Commits

Author SHA1 Message Date
billypom on debian
3d16882185 a 2025-04-24 19:22:49 -04:00
billypom on debian
18d248386c a 2025-04-24 19:20:38 -04:00
6 changed files with 82 additions and 479 deletions

View File

@ -4,15 +4,31 @@ from appdirs import user_config_dir
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from typing import Optional from typing import Optional
@dataclass @dataclass
class SQLiteMap(): class SQLiteMap:
title: str title: Optional[str] = None
artist: str artist: Optional[str] = None
album: Optional[str] = None album: Optional[str] = None
album_artist: Optional[str] = None
track_number: Optional[str] = None
genre: Optional[str] = None
length_seconds: Optional[str] = None
album_date: Optional[str] = None
codec: Optional[str] = None
filepath: Optional[str] = None
"""
db names are called FIELDS (e.g., title, track_number, length_seconds)
gui names are called HEADERS (e.g., title, track, length, year)
id3 names are called TAGS (e.g., TIT2, TPE1, TALB)
class HeaderTags(): is dataclasses rly worth it?
"""
class HeaderTags:
""" """
Utility class to converting between different "standards" for tags (headers, id3, etc) Utility class to converting between different "standards" for tags (headers, id3, etc)
@ -20,8 +36,8 @@ class HeaderTags():
`gui`: dict = "db name": "gui string" `gui`: dict = "db name": "gui string"
`id3`: dict = "db name": "id3 tag string" `id3`: dict = "db name": "id3 tag string"
`id3_keys`: dict = "id3 tag string": "db name" `id3_keys`: dict = "id3 tag string": "db name"
`editable_db_tags`: list = "list of db names that are user editable" `editable_fields`: list = "list of db names that are user editable"
`user_headers`: list = "list of db headers that the user has chosen to see in gui" `user_fields`: list = "list of db headers that the user has chosen to see in gui"
""" """
def __init__(self): def __init__(self):
@ -31,7 +47,43 @@ class HeaderTags():
) )
self.config = ConfigParser() self.config = ConfigParser()
self.config.read(cfg_file) self.config.read(cfg_file)
self.user_headers: list = str(self.config["table"]["columns"]).split(",") print("header tag config")
print(self.config)
self.user_fields: list = str(self.config["table"]["columns"]).split(",")
self.editable_fields: list = [
"title",
"artist",
"album_artist",
"album",
"track_number",
"genre",
"album_date",
]
self.fields = SQLiteMap()
self.headers = SQLiteMap(
title="title",
artist="artist",
album="album",
album_artist="alb artist",
track_number="track",
genre="genre",
codec="codec",
length_seconds="length",
album_date="year",
filepath="path",
)
# self.id3 = SQLiteMap(
# title = "TIT2",
# artist = "TPE1",
# album = "TALB",
# album_artist = "TPE2",
# track_number = "TRCK",
# genre = "TCON",
# length_seconds = "TLEN",
# album_date = "TDRC",
# codec = None,
# filepath = None
# )
self.db: dict = { self.db: dict = {
"title": "title", "title": "title",
"artist": "artist", "artist": "artist",
@ -74,20 +126,10 @@ class HeaderTags():
if v is not None: if v is not None:
self.id3_keys[v] = k self.id3_keys[v] = k
self.editable_db_tags: list = [
"title",
"artist",
"album_artist",
"album",
"track_number",
"genre",
"album_date",
]
def get_user_gui_headers(self) -> list: def get_user_gui_headers(self) -> list:
"""Returns a list of headers for the GUI""" """Returns a list of headers for the GUI"""
gui_headers = [] gui_headers = []
for db, gui in self.gui.items(): for db, gui in asdict(self.headers).items():
if db in self.user_headers: if db in self.user_fields:
gui_headers.append(gui) gui_headers.append(gui)
return gui_headers return gui_headers

View File

@ -56,9 +56,10 @@ class MetadataWindow(QDialog):
layout.addWidget(category_label) layout.addWidget(category_label)
layout.addWidget(h_separator) layout.addWidget(h_separator)
tag_sets: dict = {} tag_sets: dict[str, list] = {}
# Get a dict of all tags for all songs # Get a dict of all tags for all songs
# e.g., { "TIT2": ["song_title1", "song_title2"], ... } # e.g., { "TIT2": ["song_title1", "song_title2"], ... }
# e.g., { "title": ["song_title1", "song_title2"], ... }
for song in self.songs: for song in self.songs:
song_data = get_tags(song[0])[0] song_data = get_tags(song[0])[0]
if not song_data: if not song_data:
@ -71,7 +72,7 @@ class MetadataWindow(QDialog):
) )
return return
for key, tag in self.headers.id3.items(): for key, tag in self.headers.id3.items():
if key not in self.headers.editable_db_tags: if key not in self.headers.editable_fields:
continue continue
if tag is not None: if tag is not None:
try: try:

View File

@ -1,7 +1,5 @@
from mutagen.id3 import ID3 from mutagen.id3 import ID3
from json import load as jsonload
import DBA import DBA
from pprint import pprint
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QColor, QColor,
QDragMoveEvent, QDragMoveEvent,
@ -18,12 +16,10 @@ from PyQt5.QtWidgets import (
QAction, QAction,
QHeaderView, QHeaderView,
QMenu, QMenu,
QPlainTextEdit,
QTableView, QTableView,
QShortcut, QShortcut,
QMessageBox, QMessageBox,
QAbstractItemView, QAbstractItemView,
QVBoxLayout,
) )
from PyQt5.QtCore import ( from PyQt5.QtCore import (
QItemSelectionModel, QItemSelectionModel,
@ -32,7 +28,6 @@ from PyQt5.QtCore import (
QModelIndex, QModelIndex,
QThreadPool, QThreadPool,
pyqtSignal, pyqtSignal,
QTimer,
) )
from components.DebugWindow import DebugWindow from components.DebugWindow import DebugWindow
from components.ErrorDialog import ErrorDialog from components.ErrorDialog import ErrorDialog
@ -106,6 +101,8 @@ class MusicTable(QTableView):
) )
self.config = ConfigParser() self.config = ConfigParser()
self.config.read(cfg_file) self.config.read(cfg_file)
print("music table config:")
print(self.config)
# Threads # Threads
self.threadpool = QThreadPool self.threadpool = QThreadPool
@ -129,8 +126,6 @@ class MusicTable(QTableView):
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
# header # header
# FIXME: table headers being resized and going out window bounds
# causing some recursion errors...
self.horizontal_header: QHeaderView = self.horizontalHeader() self.horizontal_header: QHeaderView = self.horizontalHeader()
assert self.horizontal_header is not None # i hate look at linting errors assert self.horizontal_header is not None # i hate look at linting errors
self.horizontal_header.setStretchLastSection(True) self.horizontal_header.setStretchLastSection(True)
@ -344,7 +339,7 @@ class MusicTable(QTableView):
def on_sort(self): def on_sort(self):
debug("on_sort") debug("on_sort")
search_col_num = self.headers.user_headers.index("filepath") search_col_num = self.headers.user_fields.index("filepath")
selected_qmodel_index = self.find_qmodel_index_by_value( selected_qmodel_index = self.find_qmodel_index_by_value(
self.proxymodel, search_col_num, self.selected_song_filepath self.proxymodel, search_col_num, self.selected_song_filepath
) )
@ -399,18 +394,14 @@ class MusicTable(QTableView):
if isinstance(self.model2, QStandardItemModel): if isinstance(self.model2, QStandardItemModel):
debug("on_cell_data_changed") debug("on_cell_data_changed")
# get the ID of the row that was edited # get the ID of the row that was edited
id_index = self.model2.index(topLeft.row(), 0) # ID is column 0, always id_index = self.model2.index(topLeft.row(), 0)
# get the db song_id from the row # get the db song_id from the row
song_id = self.model2.data(id_index, Qt.ItemDataRole.UserRole) song_id = self.model2.data(id_index, Qt.ItemDataRole.UserRole)
# get the filepath through a series of steps... user_index = self.headers.user_fields.index("filepath")
# NOTE: filepath is always the last column filepath = self.currentIndex().siblingAtColumn(user_index).data()
filepath_column_idx = self.model2.columnCount() - 1
filepath_index = self.model2.index(topLeft.row(), filepath_column_idx)
filepath = self.model2.data(filepath_index)
# update the ID3 information # update the ID3 information
user_input_data = topLeft.data() user_input_data = topLeft.data()
# edited_column_name = self.database_columns[topLeft.column()] edited_column_name = self.headers.user_fields[topLeft.column()]
edited_column_name = self.headers.user_headers[topLeft.column()]
debug(f"on_cell_data_changed | edited column name: {edited_column_name}") debug(f"on_cell_data_changed | edited column name: {edited_column_name}")
response = set_tag(filepath, edited_column_name, user_input_data) response = set_tag(filepath, edited_column_name, user_input_data)
if response: if response:
@ -685,12 +676,12 @@ class MusicTable(QTableView):
self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore
self.model2.clear() self.model2.clear()
self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers()) self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers())
fields = ", ".join(self.headers.user_fields)
search_clause = ( search_clause = (
"title LIKE %?% AND artist LIKE %?% and album LIKE %?%" "title LIKE %?% AND artist LIKE %?% and album LIKE %?%"
if self.search_string if self.search_string
else "" else ""
) )
fields = ", ".join(self.headers.user_headers)
params = "" params = ""
if playlist_id: # Load a playlist if playlist_id: # Load a playlist
selected_playlist_id = playlist_id[0] selected_playlist_id = playlist_id[0]
@ -762,7 +753,7 @@ class MusicTable(QTableView):
print(f"load music table current filepath: {current_song_filepath}") print(f"load music table current filepath: {current_song_filepath}")
for row in range(self.model2.rowCount()): for row in range(self.model2.rowCount()):
real_index = self.model2.index( real_index = self.model2.index(
row, self.headers.user_headers.index("filepath") row, self.headers.user_fields.index("filepath")
) )
if real_index.data() == current_song_filepath: if real_index.data() == current_song_filepath:
print("is it true?") print("is it true?")
@ -864,9 +855,7 @@ class MusicTable(QTableView):
selected_rows = self.get_selected_rows() selected_rows = self.get_selected_rows()
filepaths = [] filepaths = []
for row in selected_rows: for row in selected_rows:
idx = self.proxymodel.index( idx = self.proxymodel.index(row, self.headers.user_fields.index("filepath"))
row, self.headers.user_headers.index("filepath")
)
filepaths.append(idx.data()) filepaths.append(idx.data())
return filepaths return filepaths
@ -903,8 +892,8 @@ class MusicTable(QTableView):
def set_selected_song_filepath(self) -> None: def set_selected_song_filepath(self) -> None:
"""Sets the filepath of the currently selected song""" """Sets the filepath of the currently selected song"""
try: try:
table_index = self.headers.user_headers.index("filepath") user_index = self.headers.user_fields.index("filepath")
filepath = self.currentIndex().siblingAtColumn(table_index).data() filepath = self.currentIndex().siblingAtColumn(user_index).data()
except ValueError: except ValueError:
# if the user doesnt have filepath selected as a header, retrieve the file from db # if the user doesnt have filepath selected as a header, retrieve the file from db
row = self.currentIndex().row() row = self.currentIndex().row()
@ -925,7 +914,7 @@ class MusicTable(QTableView):
# update the filepath # update the filepath
if not filepath: if not filepath:
path = self.current_song_qmodel_index.siblingAtColumn( path = self.current_song_qmodel_index.siblingAtColumn(
self.headers.user_headers.index("filepath") self.headers.user_fields.index("filepath")
).data() ).data()
self.current_song_filepath: str = path self.current_song_filepath: str = path
else: else:

View File

@ -162,6 +162,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
/ "config.ini" / "config.ini"
) )
self.config.read(self.cfg_file) self.config.read(self.cfg_file)
print("main config:")
print(self.config)
self.threadpool: QThreadPool = QThreadPool() self.threadpool: QThreadPool = QThreadPool()
# UI # UI
self.setupUi(self) self.setupUi(self)
@ -392,7 +394,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
prev_row, index.column() prev_row, index.column()
) )
prev_filepath = prev_index.siblingAtColumn( prev_filepath = prev_index.siblingAtColumn(
self.headers.user_headers.index("filepath") self.headers.user_fields.index("filepath")
).data() ).data()
if prev_filepath is None: if prev_filepath is None:
return return
@ -416,7 +418,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
next_row, index.column() next_row, index.column()
) )
next_filepath = next_index.siblingAtColumn( next_filepath = next_index.siblingAtColumn(
self.headers.user_headers.index("filepath") self.headers.user_fields.index("filepath")
).data() ).data()
if next_filepath is None: if next_filepath is None:
return return

432
ui.ui
View File

@ -1,432 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1152</width>
<height>894</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="statusTip">
<string/>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="20">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="SearchLineEdit" name="lineEditSearch"/>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutHead" stretch="1,0,6">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<layout class="QVBoxLayout" name="vlayoutAlbumArt">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<widget class="AlbumArtGraphicsView" name="albumGraphicsView">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>200</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>200</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="vLayoutSongDetails"/>
</item>
<item>
<layout class="QVBoxLayout" name="vLayoutPlaybackVisuals">
<item>
<widget class="PlotWidget" name="PlotWidget" native="true"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutMusicTable" stretch="0,10">
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="PlaylistsPane" name="playlistTreeView"/>
</item>
</layout>
</item>
<item>
<widget class="MusicTable" name="tableView"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutCurrentSongDetails" stretch="0">
<property name="spacing">
<number>6</number>
</property>
<item>
<layout class="QHBoxLayout" name="hLayoutSongDetails" stretch="0,0,0">
<item>
<widget class="QLabel" name="artistLabel"/>
</item>
<item>
<widget class="QLabel" name="titleLabel"/>
</item>
<item>
<widget class="QLabel" name="albumLabel"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutPlayback" stretch="4,0">
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<widget class="QSlider" name="playbackSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="timeHorizontalLayout2">
<item>
<widget class="QLabel" name="startTimeLabel">
<property name="font">
<font>
<family>Monospace</family>
<italic>false</italic>
</font>
</property>
<property name="text">
<string>00:00</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="slashLabel">
<property name="font">
<font>
<family>Monospace</family>
<italic>false</italic>
</font>
</property>
<property name="text">
<string>/</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="endTimeLabel">
<property name="font">
<font>
<family>Monospace</family>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>00:00</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutControls" stretch="1,0,1,1,1,0,1">
<property name="spacing">
<number>6</number>
</property>
<item>
<layout class="QHBoxLayout" name="hLayoutVolume">
<item>
<widget class="QLabel" name="volumeLabel">
<property name="text">
<string>50</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="volumeSlider">
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>101</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksAbove</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="previousButton">
<property name="font">
<font>
<pointsize>28</pointsize>
</font>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playButton">
<property name="font">
<font>
<pointsize>28</pointsize>
</font>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="nextButton">
<property name="font">
<font>
<pointsize>28</pointsize>
</font>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="speedSlider">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksAbove</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="speedLabel">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>1.00</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1152</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionOpenFiles"/>
<addaction name="actionNewPlaylist"/>
<addaction name="actionExportPlaylist"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
<addaction name="actionPreferences"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>View</string>
</property>
</widget>
<widget class="QMenu" name="menuQuick_Actions">
<property name="title">
<string>Quick-Actions</string>
</property>
<addaction name="actionScanLibraries"/>
<addaction name="actionDeleteLibrary"/>
<addaction name="actionDeleteDatabase"/>
<addaction name="actionSortColumns"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuEdit"/>
<addaction name="menuView"/>
<addaction name="menuQuick_Actions"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionPreferences">
<property name="text">
<string>Preferences</string>
</property>
<property name="statusTip">
<string>Open preferences</string>
</property>
</action>
<action name="actionScanLibraries">
<property name="text">
<string>Scan libraries</string>
</property>
</action>
<action name="actionDeleteLibrary">
<property name="text">
<string>Delete Library</string>
</property>
</action>
<action name="actionSortColumns">
<property name="text">
<string>Sort Columns</string>
</property>
</action>
<action name="actionOpenFiles">
<property name="text">
<string>Open file(s)</string>
</property>
</action>
<action name="actionDeleteDatabase">
<property name="text">
<string>Delete Database</string>
</property>
</action>
<action name="actionNewPlaylist">
<property name="text">
<string>New playlist</string>
</property>
</action>
<action name="actionExportPlaylist">
<property name="text">
<string>Export playlist</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>PlotWidget</class>
<extends>QWidget</extends>
<header>pyqtgraph</header>
<container>1</container>
</customwidget>
<customwidget>
<class>MusicTable</class>
<extends>QTableView</extends>
<header>components</header>
</customwidget>
<customwidget>
<class>AlbumArtGraphicsView</class>
<extends>QGraphicsView</extends>
<header>components</header>
</customwidget>
<customwidget>
<class>PlaylistsPane</class>
<extends>QTreeView</extends>
<header>components</header>
</customwidget>
<customwidget>
<class>SearchLineEdit</class>
<extends>QLineEdit</extends>
<header>components</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -107,6 +107,7 @@ def set_tag(filepath: str, tag_name: str, value: str):
# if tdat_tag: # if tdat_tag:
# # update TDAT if we have it # # update TDAT if we have it
# audio_file.add(tdat_tag) # audio_file.add(tdat_tag)
# Lyrics # Lyrics
if tag_name == "lyrics" or tag_name == "USLT": if tag_name == "lyrics" or tag_name == "USLT":
try: try: