Compare commits
6 Commits
e2af6dd7ac
...
42092d3d39
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42092d3d39 | ||
|
|
fbe9c2ba94 | ||
|
|
a8395d8c97 | ||
|
|
fc9a10ad52 | ||
|
|
8b644ee236 | ||
|
|
c7f7f25af0 |
@ -54,6 +54,7 @@ class Config(object):
|
|||||||
NORMALISE_ON_IMPORT = True
|
NORMALISE_ON_IMPORT = True
|
||||||
NOTE_TIME_FORMAT = "%H:%M:%S"
|
NOTE_TIME_FORMAT = "%H:%M:%S"
|
||||||
ROOT = os.environ.get('ROOT') or "/home/kae/music"
|
ROOT = os.environ.get('ROOT') or "/home/kae/music"
|
||||||
|
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
|
||||||
SCROLL_TOP_MARGIN = 3
|
SCROLL_TOP_MARGIN = 3
|
||||||
TESTMODE = True
|
TESTMODE = True
|
||||||
TOD_TIME_FORMAT = "%H:%M:%S"
|
TOD_TIME_FORMAT = "%H:%M:%S"
|
||||||
|
|||||||
@ -63,12 +63,13 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
|
|||||||
|
|
||||||
tag: TinyTag = TinyTag.get(path)
|
tag: TinyTag = TinyTag.get(path)
|
||||||
|
|
||||||
return dict(
|
d = dict(
|
||||||
title=tag.title,
|
title=tag.title,
|
||||||
artist=tag.artist,
|
artist=tag.artist,
|
||||||
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
||||||
path=path
|
path=path
|
||||||
)
|
)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
def get_relative_date(past_date: datetime, reference_date: datetime = None) \
|
def get_relative_date(past_date: datetime, reference_date: datetime = None) \
|
||||||
|
|||||||
@ -341,13 +341,14 @@ class Playlists(Base):
|
|||||||
def remove_track(self, session: Session, row: int) -> None:
|
def remove_track(self, session: Session, row: int) -> None:
|
||||||
DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
|
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
|
# Get tracks collection for this playlist
|
||||||
tracks_collections = self.tracks
|
|
||||||
# Tracks are a dictionary of tracks keyed on row
|
# Tracks are a dictionary of tracks keyed on row
|
||||||
# number. Remove the relevant row.
|
# number. Remove the relevant row.
|
||||||
del tracks_collections[row]
|
del self.tracks[row]
|
||||||
# Save the new tracks collection
|
# Save the new tracks collection
|
||||||
self.tracks = tracks_collections
|
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import argparse
|
|||||||
import os.path
|
import os.path
|
||||||
import psutil
|
import psutil
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QLineEdit,
|
QLineEdit,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
import dbconfig
|
import dbconfig
|
||||||
@ -420,14 +422,52 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
dlg = QFileDialog()
|
dlg = QFileDialog()
|
||||||
dlg.setFileMode(QFileDialog.ExistingFiles)
|
dlg.setFileMode(QFileDialog.ExistingFiles)
|
||||||
dlg.setViewMode(QFileDialog.Detail)
|
dlg.setViewMode(QFileDialog.Detail)
|
||||||
# TODO: remove hardcoded directory
|
dlg.setDirectory(Config.IMPORT_DESTINATION)
|
||||||
dlg.setDirectory(os.path.join(Config.ROOT, "Singles"))
|
|
||||||
dlg.setNameFilter("Music files (*.flac *.mp3)")
|
dlg.setNameFilter("Music files (*.flac *.mp3)")
|
||||||
|
|
||||||
if dlg.exec_():
|
if dlg.exec_():
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
|
txt: str = ""
|
||||||
|
new_tracks = []
|
||||||
for fname in dlg.selectedFiles():
|
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
|
# Add to playlist on screen
|
||||||
# If we don't specify "repaint=False", playlist will
|
# If we don't specify "repaint=False", playlist will
|
||||||
# also be saved to database
|
# also be saved to database
|
||||||
@ -979,7 +1019,8 @@ class DbDialog(QDialog):
|
|||||||
t = QListWidgetItem()
|
t = QListWidgetItem()
|
||||||
t.setText(
|
t.setText(
|
||||||
f"{track.title} - {track.artist} "
|
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)
|
t.setData(Qt.UserRole, track)
|
||||||
self.ui.matchList.addItem(t)
|
self.ui.matchList.addItem(t)
|
||||||
|
|||||||
@ -1033,7 +1033,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
|
|
||||||
DEBUG("playlist._delete_rows()")
|
DEBUG("playlist._delete_rows()")
|
||||||
|
|
||||||
rows: List[int] = sorted(
|
selected_rows: List[int] = sorted(
|
||||||
set(item.row() for item in self.selectedItems())
|
set(item.row() for item in self.selectedItems())
|
||||||
)
|
)
|
||||||
rows_to_delete: List[int] = []
|
rows_to_delete: List[int] = []
|
||||||
@ -1042,17 +1042,22 @@ class PlaylistTab(QTableWidget):
|
|||||||
row_object: Union[Tracks, Notes]
|
row_object: Union[Tracks, Notes]
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
for row in rows:
|
for row in selected_rows:
|
||||||
title = self.item(row, self.COL_TITLE).text()
|
title = self.item(row, self.COL_TITLE).text()
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
msg.setIcon(QMessageBox.Warning)
|
msg.setIcon(QMessageBox.Warning)
|
||||||
msg.setText(f"Delete '{title}'?")
|
msg.setText(f"Delete '{title}'?")
|
||||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
|
msg.setStandardButtons(
|
||||||
msg.setDefaultButton(QMessageBox.Cancel)
|
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
|
||||||
|
)
|
||||||
|
msg.setDefaultButton(QMessageBox.No)
|
||||||
msg.setWindowTitle("Delete row")
|
msg.setWindowTitle("Delete row")
|
||||||
# Store list of rows to delete
|
# Store list of rows to delete
|
||||||
if msg.exec() == QMessageBox.Yes:
|
response = msg.exec()
|
||||||
|
if response == QMessageBox.Yes:
|
||||||
rows_to_delete.append(row)
|
rows_to_delete.append(row)
|
||||||
|
elif response == QMessageBox.Cancel:
|
||||||
|
return
|
||||||
|
|
||||||
# delete in reverse row order so row numbers don't
|
# delete in reverse row order so row numbers don't
|
||||||
# change
|
# change
|
||||||
|
|||||||
@ -38,7 +38,6 @@ def main():
|
|||||||
group.add_argument('-f', '--full-update',
|
group.add_argument('-f', '--full-update',
|
||||||
action="store_true", dest="full_update",
|
action="store_true", dest="full_update",
|
||||||
default=False, help="Update database")
|
default=False, help="Update database")
|
||||||
group.add_argument('-i', '--import', dest="fname", help="Input file")
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
# Run as required
|
# Run as required
|
||||||
@ -50,18 +49,13 @@ def main():
|
|||||||
DEBUG("Full update of database")
|
DEBUG("Full update of database")
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
full_update_db(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:
|
else:
|
||||||
INFO("No action specified")
|
INFO("No action specified")
|
||||||
|
|
||||||
DEBUG("Finished")
|
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
|
Create track in database from passed path, or update database entry
|
||||||
if path already in database.
|
if path already in database.
|
||||||
@ -69,34 +63,14 @@ def create_track_from_file(session, path, normalise=None, interactive=False):
|
|||||||
Return track.
|
Return track.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if interactive:
|
if not tags:
|
||||||
msg = f"Importing {path}"
|
|
||||||
INFO(msg)
|
|
||||||
INFO("-" * len(msg))
|
|
||||||
INFO("Get track info...")
|
|
||||||
t = get_tags(path)
|
t = get_tags(path)
|
||||||
title = t['title']
|
else:
|
||||||
artist = t['artist']
|
t = tags
|
||||||
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
|
|
||||||
track = Tracks.get_or_create(session, path)
|
track = Tracks.get_or_create(session, path)
|
||||||
track.title = title
|
track.title = t['title']
|
||||||
track.artist = artist
|
track.artist = t['artist']
|
||||||
if interactive:
|
|
||||||
INFO("Parse for start, fade and silence...")
|
|
||||||
audio = get_audio_segment(path)
|
audio = get_audio_segment(path)
|
||||||
track.duration = len(audio)
|
track.duration = len(audio)
|
||||||
track.start_gap = leading_silence(audio)
|
track.start_gap = leading_silence(audio)
|
||||||
@ -108,8 +82,6 @@ def create_track_from_file(session, path, normalise=None, interactive=False):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
|
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
|
||||||
if interactive:
|
|
||||||
INFO("Normalise...")
|
|
||||||
# Check type
|
# Check type
|
||||||
ftype = os.path.splitext(path)[1][1:]
|
ftype = os.path.splitext(path)[1][1:]
|
||||||
if ftype not in ['mp3', 'flac']:
|
if ftype not in ['mp3', 'flac']:
|
||||||
@ -252,7 +224,7 @@ def update_db(session):
|
|||||||
|
|
||||||
for path in list(os_paths - db_paths):
|
for path in list(os_paths - db_paths):
|
||||||
DEBUG(f"utilities.update_db: {path=} not in database")
|
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))
|
track = Tracks.get_by_filename(session, os.path.basename(path))
|
||||||
if not track:
|
if not track:
|
||||||
messages.append(f"{path} missing from database: {path}")
|
messages.append(f"{path} missing from database: {path}")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user