a
This commit is contained in:
parent
491e134d94
commit
18d248386c
@ -4,15 +4,31 @@ from appdirs import user_config_dir
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class SQLiteMap():
|
||||
title: str
|
||||
artist: str
|
||||
class SQLiteMap:
|
||||
title: Optional[str] = None
|
||||
artist: 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)
|
||||
|
||||
@ -20,8 +36,8 @@ class HeaderTags():
|
||||
`gui`: dict = "db name": "gui string"
|
||||
`id3`: dict = "db name": "id3 tag string"
|
||||
`id3_keys`: dict = "id3 tag string": "db name"
|
||||
`editable_db_tags`: 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"
|
||||
`editable_fields`: list = "list of db names that are user editable"
|
||||
`user_fields`: list = "list of db headers that the user has chosen to see in gui"
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@ -31,7 +47,43 @@ class HeaderTags():
|
||||
)
|
||||
self.config = ConfigParser()
|
||||
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 = {
|
||||
"title": "title",
|
||||
"artist": "artist",
|
||||
@ -74,20 +126,10 @@ class HeaderTags():
|
||||
if v is not None:
|
||||
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:
|
||||
"""Returns a list of headers for the GUI"""
|
||||
gui_headers = []
|
||||
for db, gui in self.gui.items():
|
||||
if db in self.user_headers:
|
||||
for db, gui in asdict(self.headers).items():
|
||||
if db in self.user_fields:
|
||||
gui_headers.append(gui)
|
||||
return gui_headers
|
||||
|
||||
@ -56,9 +56,10 @@ class MetadataWindow(QDialog):
|
||||
layout.addWidget(category_label)
|
||||
layout.addWidget(h_separator)
|
||||
|
||||
tag_sets: dict = {}
|
||||
tag_sets: dict[str, list] = {}
|
||||
# Get a dict of all tags for all songs
|
||||
# e.g., { "TIT2": ["song_title1", "song_title2"], ... }
|
||||
# e.g., { "title": ["song_title1", "song_title2"], ... }
|
||||
for song in self.songs:
|
||||
song_data = get_tags(song[0])[0]
|
||||
if not song_data:
|
||||
@ -71,7 +72,7 @@ class MetadataWindow(QDialog):
|
||||
)
|
||||
return
|
||||
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
|
||||
if tag is not None:
|
||||
try:
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
from mutagen.id3 import ID3
|
||||
from json import load as jsonload
|
||||
import DBA
|
||||
from pprint import pprint
|
||||
from PyQt5.QtGui import (
|
||||
QColor,
|
||||
QDragMoveEvent,
|
||||
@ -18,12 +16,10 @@ from PyQt5.QtWidgets import (
|
||||
QAction,
|
||||
QHeaderView,
|
||||
QMenu,
|
||||
QPlainTextEdit,
|
||||
QTableView,
|
||||
QShortcut,
|
||||
QMessageBox,
|
||||
QAbstractItemView,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from PyQt5.QtCore import (
|
||||
QItemSelectionModel,
|
||||
@ -32,7 +28,6 @@ from PyQt5.QtCore import (
|
||||
QModelIndex,
|
||||
QThreadPool,
|
||||
pyqtSignal,
|
||||
QTimer,
|
||||
)
|
||||
from components.DebugWindow import DebugWindow
|
||||
from components.ErrorDialog import ErrorDialog
|
||||
@ -104,6 +99,8 @@ class MusicTable(QTableView):
|
||||
)
|
||||
self.config = ConfigParser()
|
||||
self.config.read(cfg_file)
|
||||
print("music table config:")
|
||||
print(self.config)
|
||||
|
||||
# Threads
|
||||
self.threadpool = QThreadPool
|
||||
@ -127,8 +124,6 @@ class MusicTable(QTableView):
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
# header
|
||||
# FIXME: table headers being resized and going out window bounds
|
||||
# causing some recursion errors...
|
||||
self.horizontal_header: QHeaderView = self.horizontalHeader()
|
||||
assert self.horizontal_header is not None # i hate look at linting errors
|
||||
self.horizontal_header.setStretchLastSection(True)
|
||||
@ -342,7 +337,7 @@ class MusicTable(QTableView):
|
||||
|
||||
def on_sort(self):
|
||||
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(
|
||||
self.proxymodel, search_col_num, self.selected_song_filepath
|
||||
)
|
||||
@ -397,18 +392,14 @@ class MusicTable(QTableView):
|
||||
if isinstance(self.model2, QStandardItemModel):
|
||||
debug("on_cell_data_changed")
|
||||
# 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
|
||||
song_id = self.model2.data(id_index, Qt.ItemDataRole.UserRole)
|
||||
# get the filepath through a series of steps...
|
||||
# NOTE: filepath is always the last column
|
||||
filepath_column_idx = self.model2.columnCount() - 1
|
||||
filepath_index = self.model2.index(topLeft.row(), filepath_column_idx)
|
||||
filepath = self.model2.data(filepath_index)
|
||||
user_index = self.headers.user_fields.index("filepath")
|
||||
filepath = self.currentIndex().siblingAtColumn(user_index).data()
|
||||
# update the ID3 information
|
||||
user_input_data = topLeft.data()
|
||||
# edited_column_name = self.database_columns[topLeft.column()]
|
||||
edited_column_name = self.headers.user_headers[topLeft.column()]
|
||||
edited_column_name = self.headers.user_fields[topLeft.column()]
|
||||
debug(f"on_cell_data_changed | edited column name: {edited_column_name}")
|
||||
response = set_tag(filepath, edited_column_name, user_input_data)
|
||||
if response:
|
||||
@ -677,7 +668,7 @@ class MusicTable(QTableView):
|
||||
self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore
|
||||
self.model2.clear()
|
||||
self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers())
|
||||
fields = ", ".join(self.headers.user_headers)
|
||||
fields = ", ".join(self.headers.user_fields)
|
||||
if playlist_id: # Load a playlist
|
||||
# Fetch playlist data
|
||||
selected_playlist_id = playlist_id[0]
|
||||
@ -730,13 +721,15 @@ class MusicTable(QTableView):
|
||||
# reloading the model destroys and makes new indexes
|
||||
# so we look for the new index of the current song on load
|
||||
current_song_filepath = self.get_current_song_filepath()
|
||||
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()):
|
||||
real_index = self.model2.index(row, self.headers.user_headers.index("filepath"))
|
||||
real_index = self.model2.index(
|
||||
row, self.headers.user_fields.index("filepath")
|
||||
)
|
||||
if real_index.data() == current_song_filepath:
|
||||
print('is it true?')
|
||||
print(f'{real_index.data()} == {current_song_filepath}')
|
||||
print('load music table real index:')
|
||||
print("is it true?")
|
||||
print(f"{real_index.data()} == {current_song_filepath}")
|
||||
print("load music table real index:")
|
||||
print(real_index)
|
||||
self.current_song_qmodel_index = real_index
|
||||
self.model2.layoutChanged.emit() # emits a signal that the view should be updated
|
||||
@ -833,9 +826,7 @@ class MusicTable(QTableView):
|
||||
selected_rows = self.get_selected_rows()
|
||||
filepaths = []
|
||||
for row in selected_rows:
|
||||
idx = self.proxymodel.index(
|
||||
row, self.headers.user_headers.index("filepath")
|
||||
)
|
||||
idx = self.proxymodel.index(row, self.headers.user_fields.index("filepath"))
|
||||
filepaths.append(idx.data())
|
||||
return filepaths
|
||||
|
||||
@ -850,7 +841,9 @@ class MusicTable(QTableView):
|
||||
return []
|
||||
selected_rows = set(index.row() for index in indexes)
|
||||
id_list = [
|
||||
self.proxymodel.data(self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole)
|
||||
self.proxymodel.data(
|
||||
self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole
|
||||
)
|
||||
for row in selected_rows
|
||||
]
|
||||
return id_list
|
||||
@ -870,14 +863,18 @@ class MusicTable(QTableView):
|
||||
def set_selected_song_filepath(self) -> None:
|
||||
"""Sets the filepath of the currently selected song"""
|
||||
try:
|
||||
table_index = self.headers.user_headers.index("filepath")
|
||||
filepath = self.currentIndex().siblingAtColumn(table_index).data()
|
||||
user_index = self.headers.user_fields.index("filepath")
|
||||
filepath = self.currentIndex().siblingAtColumn(user_index).data()
|
||||
except ValueError:
|
||||
# if the user doesnt have filepath selected as a header, retrieve the file from db
|
||||
row = self.currentIndex().row()
|
||||
id = self.proxymodel.data(self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole)
|
||||
id = self.proxymodel.data(
|
||||
self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole
|
||||
)
|
||||
with DBA.DBAccess() as db:
|
||||
filepath = db.query('SELECT filepath FROM song WHERE id = ?', (id,))[0][0]
|
||||
filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][
|
||||
0
|
||||
]
|
||||
self.selected_song_filepath = filepath
|
||||
|
||||
def set_current_song_filepath(self, filepath=None) -> None:
|
||||
@ -887,7 +884,9 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
# update the filepath
|
||||
if not filepath:
|
||||
path = self.current_song_qmodel_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data()
|
||||
path = self.current_song_qmodel_index.siblingAtColumn(
|
||||
self.headers.user_fields.index("filepath")
|
||||
).data()
|
||||
self.current_song_filepath: str = path
|
||||
else:
|
||||
self.current_song_filepath = filepath
|
||||
|
||||
38
main.py
38
main.py
@ -41,7 +41,13 @@ from PyQt5.QtCore import (
|
||||
QThreadPool,
|
||||
QRunnable,
|
||||
)
|
||||
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist, QMultimedia
|
||||
from PyQt5.QtMultimedia import (
|
||||
QMediaPlayer,
|
||||
QMediaContent,
|
||||
QAudioProbe,
|
||||
QMediaPlaylist,
|
||||
QMultimedia,
|
||||
)
|
||||
from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent
|
||||
from utils import (
|
||||
delete_album_art,
|
||||
@ -50,7 +56,7 @@ from utils import (
|
||||
initialize_db,
|
||||
add_files_to_database,
|
||||
set_album_art,
|
||||
id3_remap
|
||||
id3_remap,
|
||||
)
|
||||
from components import (
|
||||
HeaderTags,
|
||||
@ -155,6 +161,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
/ "config.ini"
|
||||
)
|
||||
self.config.read(self.cfg_file)
|
||||
print("main config:")
|
||||
print(self.config)
|
||||
self.threadpool: QThreadPool = QThreadPool()
|
||||
# UI
|
||||
self.setupUi(self)
|
||||
@ -328,7 +336,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
file_url = media.canonicalUrl().toLocalFile()
|
||||
metadata = id3_remap(get_tags(file_url)[0])
|
||||
if metadata is not None:
|
||||
self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"], file_url)
|
||||
self.set_ui_metadata(
|
||||
metadata["title"], metadata["artist"], metadata["album"], file_url
|
||||
)
|
||||
|
||||
def on_volume_changed(self) -> None:
|
||||
"""Handles volume changes"""
|
||||
@ -374,8 +384,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
row: int = index.row()
|
||||
prev_row: int = row - 1
|
||||
prev_index: QModelIndex = self.tableView.proxymodel.index(prev_row, index.column())
|
||||
prev_filepath = prev_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data()
|
||||
prev_index: QModelIndex = self.tableView.proxymodel.index(
|
||||
prev_row, index.column()
|
||||
)
|
||||
prev_filepath = prev_index.siblingAtColumn(
|
||||
self.headers.user_fields.index("filepath")
|
||||
).data()
|
||||
if prev_filepath is None:
|
||||
return
|
||||
|
||||
@ -394,8 +408,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
row: int = index.row()
|
||||
next_row: int = row + 1
|
||||
next_index: QModelIndex = self.tableView.proxymodel.index(next_row, index.column())
|
||||
next_filepath = next_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data()
|
||||
next_index: QModelIndex = self.tableView.proxymodel.index(
|
||||
next_row, index.column()
|
||||
)
|
||||
next_filepath = next_index.siblingAtColumn(
|
||||
self.headers.user_fields.index("filepath")
|
||||
).data()
|
||||
if next_filepath is None:
|
||||
return
|
||||
self.play_audio_file(next_filepath)
|
||||
@ -471,7 +489,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
|
||||
filepath default value = `tableView.current_song_filepath`
|
||||
"""
|
||||
print('play audio file')
|
||||
print("play audio file")
|
||||
if not filepath:
|
||||
filepath = self.tableView.get_selected_song_filepath()
|
||||
metadata = id3_remap(get_tags(filepath)[0])
|
||||
@ -487,7 +505,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# assign "now playing" labels & album artwork
|
||||
if metadata is not None:
|
||||
self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"], filepath)
|
||||
self.set_ui_metadata(
|
||||
metadata["title"], metadata["artist"], metadata["album"], filepath
|
||||
)
|
||||
|
||||
def set_ui_metadata(self, title, artist, album, filepath):
|
||||
"""
|
||||
|
||||
6
ui.ui
6
ui.ui
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1152</width>
|
||||
<height>894</height>
|
||||
<width>953</width>
|
||||
<height>739</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -310,7 +310,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1152</width>
|
||||
<width>953</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
||||
@ -107,6 +107,7 @@ def set_tag(filepath: str, tag_name: str, value: str):
|
||||
# if tdat_tag:
|
||||
# # update TDAT if we have it
|
||||
# audio_file.add(tdat_tag)
|
||||
|
||||
# Lyrics
|
||||
if tag_name == "lyrics" or tag_name == "USLT":
|
||||
try:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user