Compare commits

..

6 Commits

Author SHA1 Message Date
Keith Edmunds
42092d3d39 Add 'last played' time to track select from database box
Fixes #116
2022-06-04 23:05:39 +01:00
Keith Edmunds
fbe9c2ba94 Fix deleting multiple rows
Also allow mass delete to be cancelled.

Fixes #115
2022-06-04 22:56:38 +01:00
Keith Edmunds
a8395d8c97 Fix background importing and duplicate checking 2022-06-04 22:32:22 +01:00
Keith Edmunds
fc9a10ad52 Tidy up playlist.remove_track 2022-05-02 17:32:29 +01:00
Keith Edmunds
8b644ee236 Clarify comment 2022-05-02 16:09:29 +01:00
Keith Edmunds
c7f7f25af0 Run file import in separate thread 2022-04-19 15:25:15 +01:00
6 changed files with 75 additions and 54 deletions

View File

@ -54,6 +54,7 @@ class Config(object):
NORMALISE_ON_IMPORT = True
NOTE_TIME_FORMAT = "%H:%M:%S"
ROOT = os.environ.get('ROOT') or "/home/kae/music"
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
SCROLL_TOP_MARGIN = 3
TESTMODE = True
TOD_TIME_FORMAT = "%H:%M:%S"

View File

@ -63,12 +63,13 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
tag: TinyTag = TinyTag.get(path)
return dict(
d = dict(
title=tag.title,
artist=tag.artist,
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
path=path
)
return d
def get_relative_date(past_date: datetime, reference_date: datetime = None) \

View File

@ -341,13 +341,14 @@ class Playlists(Base):
def remove_track(self, session: Session, row: int) -> None:
DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
# Refresh self first (this is necessary when calling remove_track
# multiple times before session.commit())
session.refresh(self)
# Get tracks collection for this playlist
tracks_collections = self.tracks
# Tracks are a dictionary of tracks keyed on row
# number. Remove the relevant row.
del tracks_collections[row]
del self.tracks[row]
# Save the new tracks collection
self.tracks = tracks_collections
session.flush()

View File

@ -4,6 +4,7 @@ import argparse
import os.path
import psutil
import sys
import threading
import urllib.parse
import webbrowser
@ -24,6 +25,7 @@ from PyQt5.QtWidgets import (
QLineEdit,
QListWidgetItem,
QMainWindow,
QMessageBox,
)
import dbconfig
@ -420,14 +422,52 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = QFileDialog()
dlg.setFileMode(QFileDialog.ExistingFiles)
dlg.setViewMode(QFileDialog.Detail)
# TODO: remove hardcoded directory
dlg.setDirectory(os.path.join(Config.ROOT, "Singles"))
dlg.setDirectory(Config.IMPORT_DESTINATION)
dlg.setNameFilter("Music files (*.flac *.mp3)")
if dlg.exec_():
with Session() as session:
txt: str = ""
new_tracks = []
for fname in dlg.selectedFiles():
track = create_track_from_file(session, fname)
tags = helpers.get_tags(fname)
new_tracks.append((fname, tags))
title = tags['title']
artist = tags['artist']
possible_matches = Tracks.search_titles(session, title)
if possible_matches:
txt += 'Similar to new track '
txt += f'"{title}" by "{artist} ({fname})":\n\n'
for track in possible_matches:
txt += f' "{track.title}" by {track.artist}'
txt += f' ({track.path})\n'
txt += "\n"
# Check whether to proceed if there were potential matches
if txt:
txt += "Proceed with import?"
result = QMessageBox.question(self,
"Possible duplicates",
txt,
QMessageBox.Ok,
QMessageBox.Cancel
)
if result == QMessageBox.Cancel:
return
# Import in separate thread
thread = threading.Thread(target=self._import_tracks,
args=(new_tracks,))
thread.start()
def _import_tracks(self, tracks: list):
"""
Import passed files. Don't use parent session as that may be invalid
by the time we need it.
"""
with Session() as session:
for (fname, tags) in tracks:
track = create_track_from_file(session, fname, tags=tags)
# Add to playlist on screen
# If we don't specify "repaint=False", playlist will
# also be saved to database
@ -979,7 +1019,8 @@ class DbDialog(QDialog):
t = QListWidgetItem()
t.setText(
f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}]"
f"[{helpers.ms_to_mmss(track.duration)}] "
f"({helpers.get_relative_date(track.lastplayed)})"
)
t.setData(Qt.UserRole, track)
self.ui.matchList.addItem(t)

View File

@ -1033,7 +1033,7 @@ class PlaylistTab(QTableWidget):
DEBUG("playlist._delete_rows()")
rows: List[int] = sorted(
selected_rows: List[int] = sorted(
set(item.row() for item in self.selectedItems())
)
rows_to_delete: List[int] = []
@ -1042,17 +1042,22 @@ class PlaylistTab(QTableWidget):
row_object: Union[Tracks, Notes]
with Session() as session:
for row in rows:
for row in selected_rows:
title = self.item(row, self.COL_TITLE).text()
msg = QMessageBox(self)
msg.setIcon(QMessageBox.Warning)
msg.setText(f"Delete '{title}'?")
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
msg.setDefaultButton(QMessageBox.Cancel)
msg.setStandardButtons(
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
)
msg.setDefaultButton(QMessageBox.No)
msg.setWindowTitle("Delete row")
# Store list of rows to delete
if msg.exec() == QMessageBox.Yes:
response = msg.exec()
if response == QMessageBox.Yes:
rows_to_delete.append(row)
elif response == QMessageBox.Cancel:
return
# delete in reverse row order so row numbers don't
# change

View File

@ -38,7 +38,6 @@ def main():
group.add_argument('-f', '--full-update',
action="store_true", dest="full_update",
default=False, help="Update database")
group.add_argument('-i', '--import', dest="fname", help="Input file")
args = p.parse_args()
# Run as required
@ -50,18 +49,13 @@ def main():
DEBUG("Full update of database")
with Session() as session:
full_update_db(session)
elif args.fname:
fname = os.path.realpath(args.fname)
with Session() as session:
create_track_from_file(session, fname, interactive=True)
else:
INFO("No action specified")
DEBUG("Finished")
def create_track_from_file(session, path, normalise=None, interactive=False):
def create_track_from_file(session, path, normalise=None, tags=None):
"""
Create track in database from passed path, or update database entry
if path already in database.
@ -69,34 +63,14 @@ def create_track_from_file(session, path, normalise=None, interactive=False):
Return track.
"""
if interactive:
msg = f"Importing {path}"
INFO(msg)
INFO("-" * len(msg))
INFO("Get track info...")
if not tags:
t = get_tags(path)
title = t['title']
artist = t['artist']
if interactive:
INFO(f" Title: \"{title}\"")
INFO(f" Artist: \"{artist}\"")
# Check for duplicate
if interactive:
tracks = Tracks.search_titles(session, title)
if tracks:
print("Found the following possible matches:")
for track in tracks:
print(f'"{track.title}" by {track.artist}')
response = input("Continue [c] or abort [a]?")
if not response:
return
if response[0].lower() not in ['c', 'y']:
return
else:
t = tags
track = Tracks.get_or_create(session, path)
track.title = title
track.artist = artist
if interactive:
INFO("Parse for start, fade and silence...")
track.title = t['title']
track.artist = t['artist']
audio = get_audio_segment(path)
track.duration = len(audio)
track.start_gap = leading_silence(audio)
@ -108,8 +82,6 @@ def create_track_from_file(session, path, normalise=None, interactive=False):
session.commit()
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
if interactive:
INFO("Normalise...")
# Check type
ftype = os.path.splitext(path)[1][1:]
if ftype not in ['mp3', 'flac']:
@ -252,7 +224,7 @@ def update_db(session):
for path in list(os_paths - db_paths):
DEBUG(f"utilities.update_db: {path=} not in database")
# is filename in database?
# is filename in database with a different path?
track = Tracks.get_by_filename(session, os.path.basename(path))
if not track:
messages.append(f"{path} missing from database: {path}")