Compare commits

...

19 Commits

Author SHA1 Message Date
Keith Edmunds
0e3e30391b Don't grow window when track title too long
Use an elided text box, set wrapping and max height for label.

Fixes #26
2021-08-15 16:03:48 +01:00
Keith Edmunds
246b0d4915 Improve full database update sanity check 2021-08-15 13:04:30 +01:00
Keith Edmunds
fcf4ba3eb9 Implement full database scan 2021-08-15 12:52:50 +01:00
Keith Edmunds
a7d9252619 Move Fade button to right of Stop
Fixes #50
2021-08-15 11:22:35 +01:00
Keith Edmunds
d4f542cc29 Warn when trying to delete playing or next track 2021-08-15 11:17:09 +01:00
Keith Edmunds
2c9f041838 Show last track in playlist as playing when it is
Fixes #52
2021-08-15 11:06:08 +01:00
Keith Edmunds
90a8209551 Clean up of musicmuster.py 2021-08-15 10:40:28 +01:00
Keith Edmunds
c0752407b9 Handle next track not found consistently
Highlight in red, don't set as next track.
Fixes #51
2021-08-15 10:13:42 +01:00
Keith Edmunds
87fb74b14f Tidy up model.py 2021-08-15 09:21:32 +01:00
Keith Edmunds
6336eb9215 add test file for fades 2021-08-15 00:21:40 +01:00
Keith Edmunds
ee74deaa49 Clean up when tracks ends and next track is not immediately played. 2021-08-15 00:20:30 +01:00
Keith Edmunds
00cae6dc52 Fix up silence detection from last commit 2021-08-15 00:03:52 +01:00
Keith Edmunds
11e3536801 Emit INFO message during database scan 2021-08-14 23:53:43 +01:00
Keith Edmunds
427afee8da Change algorithm to detect fade point 2021-08-14 23:52:31 +01:00
Keith Edmunds
b4da349a8c Remove unused function last_show() 2021-08-14 23:07:30 +01:00
Keith Edmunds
0836f74d17 Improve 'last played' strings 2021-08-14 23:06:16 +01:00
Keith Edmunds
89d49f3e34 Merge 2021-08-14 18:44:05 +01:00
Keith Edmunds
e813a01e14 Improve track info box 2021-08-14 18:29:29 +01:00
Keith Edmunds
72e3ef69ff Handle files not found in database update
Fixes #37
Fixes #36
2021-08-14 18:26:59 +01:00
8 changed files with 382 additions and 161 deletions

View File

@ -1,9 +1,16 @@
from datetime import datetime, date
from datetime import datetime
from PyQt5.QtWidgets import QMessageBox
def get_relative_date(past_date, reference_date=None):
"""
Return relative date as string.
Return how long before reference_date past_date is as string.
Params:
@past_date: datetime
@reference_date: datetime, defaults to current date and time
@return: string
"""
if not past_date:
@ -19,7 +26,21 @@ def get_relative_date(past_date, reference_date=None):
if weeks == days == 0:
# Played today, so return time instead
return past_date.strftime("%H:%M")
return f"{weeks} weeks, {days} days ago"
if weeks == 1:
weeks_str = "week"
else:
weeks_str = "weeks"
if days == 1:
days_str = "day"
else:
days_str = "days"
return f"{weeks} {weeks_str}, {days} {days_str} ago"
def show_warning(title, msg):
"Display a warning to user"
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def ms_to_mmss(ms, decimals=0, negative=False):

View File

@ -53,6 +53,8 @@ class Notes(Base):
@staticmethod
def add_note(session, playlist_id, row, text):
"Add note"
DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})")
note = Notes()
note.playlist_id = playlist_id
@ -64,16 +66,13 @@ class Notes(Base):
@staticmethod
def delete_note(session, id):
"Delete note"
DEBUG(f"delete_note(id={id}")
session.query(Notes).filter(Notes.id == id).delete()
session.commit()
# Not currently used 1 June 2021
# @staticmethod
# def get_note(session, id):
# return session.query(Notes).filter(Notes.id == id).one()
@classmethod
def update_note(cls, session, id, row, text=None):
"""
@ -99,6 +98,8 @@ class Playdates(Base):
@staticmethod
def add_playdate(session, track):
"Record that track was played"
DEBUG(f"add_playdate(track={track})")
pd = Playdates()
pd.lastplayed = datetime.now()
@ -119,6 +120,17 @@ class Playdates(Base):
else:
return None
@staticmethod
def remove_track(session, track_id):
"""
Remove all records of track_id
"""
session.query(Playdates).filter(
Playdates.track_id == track_id,
).delete()
session.commit()
class Playlists(Base):
"""
@ -293,18 +305,11 @@ class PlaylistTracks(Base):
session.commit()
@staticmethod
def get_playlists_containing_track_id(session, track_id):
def get_track_playlists(session, track_id):
"Return all PlaylistTracks objects with this track_id"
playlists = []
playlist_ids = session.query(PlaylistTracks.playlist_id).filter(
return session.query(PlaylistTracks).filter(
PlaylistTracks.track_id == track_id).all()
for p in playlist_ids:
playlist = session.query(Playlists).filter(
Playlists.id == p[0]).first()
if playlist:
playlists.append(playlist)
return playlists
@staticmethod
def move_track(session, from_playlist_id, row, to_playlist_id):
@ -447,6 +452,22 @@ class Tracks(Base):
f"<Track(id={self.id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>"
)
# Not currently used 1 June 2021
# @staticmethod
# def get_note(session, id):
# return session.query(Notes).filter(Notes.id == id).one()
@staticmethod
def get_all_paths(session):
"Return a list of paths of all tracks"
return [a[0] for a in session.query(Tracks.path).all()]
@staticmethod
def get_all_tracks(session):
"Return a list of all tracks"
return session.query(Tracks).all()
@classmethod
def get_or_create(cls, session, path):
@ -468,30 +489,6 @@ class Tracks(Base):
ERROR(f"Can't find track id {id}")
return None
@staticmethod
def get_all_paths(session):
"Return a list of paths of all tracks"
return [a[0] for a in session.query(Tracks.path).all()]
@staticmethod
def get_path(session, id):
try:
return session.query(Tracks.path).filter(Tracks.id == id).one()[0]
except NoResultFound:
ERROR(f"Can't find track id {id}")
return None
@staticmethod
def get_track(session, id):
try:
DEBUG(f"Tracks.get_track(track_id={id})")
track = session.query(Tracks).filter(Tracks.id == id).one()
return track
except NoResultFound:
ERROR(f"get_track({id}): not found")
return None
@staticmethod
def get_track_from_filename(session, filename):
"""
@ -519,6 +516,29 @@ class Tracks(Base):
return session.query(Tracks).filter(Tracks.path == path).first()
@staticmethod
def get_path(session, track_id):
"Return path of passed track_id, or None"
try:
return session.query(Tracks.path).filter(
Tracks.id == track_id).one()[0]
except NoResultFound:
ERROR(f"Can't find track id {track_id}")
return None
@staticmethod
def get_track(session, track_id):
"Return track or None"
try:
DEBUG(f"Tracks.get_track(track_id={track_id})")
track = session.query(Tracks).filter(Tracks.id == track_id).one()
return track
except NoResultFound:
ERROR(f"get_track({track_id}): not found")
return None
@staticmethod
def remove_path(session, path):
"Remove track with passed path from database"
@ -527,9 +547,33 @@ class Tracks(Base):
try:
session.query(Tracks).filter(Tracks.path == path).delete()
session.commit()
except IntegrityError as exception:
ERROR(f"Can't remove track with {path=} ({exception=})")
@staticmethod
def search(session, title=None, artist=None, duration=None):
"""
Return any tracks matching passed criteria
"""
DEBUG(
f"Tracks.search({title=}, {artist=}), {duration=})"
)
if not title and not artist and not duration:
return None
q = session.query(Tracks).filter(False)
if title:
q = q.filter(Tracks.title == title)
if artist:
q = q.filter(Tracks.artist == artist)
if duration:
q = q.filter(Tracks.duration == duration)
return q.all()
@staticmethod
def search_titles(session, text):
return (

View File

@ -8,17 +8,17 @@ import urllib.parse
from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION
from slugify import slugify
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QFontMetrics, QPainter
from PyQt5.QtWidgets import (
QApplication,
QDialog,
QFileDialog,
QInputDialog,
QLabel,
QListWidgetItem,
QMainWindow,
QMessageBox,
)
import helpers
@ -34,6 +34,21 @@ from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
from ui.main_window_ui import Ui_MainWindow
class ElideLabel(QLabel):
"""
From https://stackoverflow.com/questions/11446478/
pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize
"""
def paintEvent(self, event):
painter = QPainter(self)
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
painter.drawText(self.rect(), self.alignment(), elided)
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__(parent)
@ -139,10 +154,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next)
self.actionSearch_database.triggered.connect(self.search_database)
self.actionSelect_next_track.triggered.connect(self.select_next_track)
self.actionSelect_next_track.triggered.connect(self.select_next_row)
self.actionSelect_played_tracks.triggered.connect(self.select_played)
self.actionSelect_previous_track.triggered.connect(
self.select_previous_track)
self.select_previous_row)
self.actionSelect_unplayed_tracks.triggered.connect(
self.select_unplayed)
self.actionSetNext.triggered.connect(self.set_next_track)
@ -224,13 +239,17 @@ class Window(QMainWindow, Ui_MainWindow):
def end_of_track_actions(self):
"Clean up after track played"
# Set self.playing to False so that tick() doesn't see
# player=None and kick off end-of-track actions
self.playing = False
# Clean up metadata
self.previous_track = self.current_track
self.current_track_playlist_tab.play_stopped()
self.current_track_playlist_tab.clear_current()
self.current_track_playlist_tab = None
if self.current_track_playlist_tab:
self.current_track_playlist_tab.play_stopped()
self.current_track_playlist_tab.clear_current()
self.current_track_playlist_tab = None
self.current_track = None
self.playing = False
# Clean up display
self.label_end_timer.setText("00:00")
@ -238,6 +257,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_fade.setStyleSheet("")
self.update_headers()
# Enable controls
self.enable_play_next_controls()
def export_playlist_tab(self):
"Export the current playlist to an m3u file"
@ -284,10 +306,6 @@ class Window(QMainWindow, Ui_MainWindow):
if not self.current_track:
return
# Set self.playing to False so that tick() doesn't see
# player=None and kick off end-of-track actions
self.playing = False
self.enable_play_next_controls()
self.previous_track_position = self.music.fade()
self.end_of_track_actions()
@ -414,17 +432,15 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.play(self.current_track.path)
# Update metadata
# TODO is this valid if next track is on different playlist?
# Get next track for this playlist. May be None if there is
# no automatic next track, and may later be overriden by
# user selecting a different track on this or another
# playlist.
next_track_id = self.current_track_playlist_tab.play_started()
if next_track_id is not None:
self.next_track = Tracks.get_track(session, next_track_id)
self.next_track_playlist_tab = self.current_track_playlist_tab
# Check we can read it
if not self.file_is_readable(self.next_track.path):
self.show_warning(
"Can't read next track",
self.next_track.path)
else:
self.next_track = self.next_track_playlist_tab = None
@ -465,20 +481,20 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_db = Playlists.open(session, dlg.plid)
self.load_playlist(session, playlist_db)
def select_next_track(self):
"Select next or first track in playlist"
def select_next_row(self):
"Select next or first row in playlist"
self.visible_playlist_tab().select_next_track()
self.visible_playlist_tab().select_next_row()
def select_played(self):
"Select all played tracks in playlist"
self.visible_playlist_tab().select_played_tracks()
def select_previous_track(self):
"Select previous or first track in playlist"
def select_previous_row(self):
"Select previous or first row in playlist"
self.visible_playlist_tab().select_previous_track()
self.visible_playlist_tab().select_previous_row()
def set_next_track(self, next_track_id=None):
"Set selected track as next"
@ -500,11 +516,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.visible_playlist_tab().select_unplayed_tracks()
def show_warning(self, title, msg):
"Display a warning to user"
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def song_info_search(self):
"""
Open browser tabs for Wikipedia, searching for
@ -533,15 +544,15 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop()")
self.stop_playing(fade=False)
self.enable_play_next_controls()
def stop_playing(self, fade=True):
"Stop playing current track"
DEBUG("musicmuster.stop_playing()", True)
DEBUG(f"musicmuster.stop_playing({fade=})", True)
if not self.music.playing():
DEBUG("musicmuster.stop_playing(): not playing", True)
self.end_of_track_actions()
return
self.previous_track_position = self.music.get_position()
@ -549,8 +560,8 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop_playing(): fading music", True)
self.music.fade()
else:
self.music.stop()
DEBUG("musicmuster.stop_playing(): stopping music", True)
self.music.stop()
self.end_of_track_actions()
@ -618,16 +629,20 @@ class Window(QMainWindow, Ui_MainWindow):
# Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
# Time to silence
# If silent in the next 5 seconds, put warning colour on
# time to silence box and enable play controls
if time_to_silence <= 5500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
)
self.enable_play_next_controls()
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
# Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls
elif time_to_fade <= 5500:
self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"

View File

@ -16,7 +16,7 @@ import os
from config import Config
from datetime import datetime, timedelta
from helpers import get_relative_date
from helpers import get_relative_date, show_warning
from log import DEBUG, ERROR
from model import (
Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks
@ -155,8 +155,8 @@ class PlaylistTab(QTableWidget):
self.menu.addSeparator()
act_delete = self.menu.addAction('Delete')
act_delete.triggered.connect(lambda: self._delete_row(row))
act_delete = self.menu.addAction('Info')
act_delete.triggered.connect(lambda: self._info_row(row))
act_info = self.menu.addAction('Info')
act_info.triggered.connect(lambda: self._info_row(row))
return super(PlaylistTab, self).eventFilter(source, event)
@ -357,7 +357,10 @@ class PlaylistTab(QTableWidget):
# Scroll to put current track in centre
scroll_to = self.item(current_row, self.COL_INDEX)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter)
next_track_id = self._mark_next_track()
# Get next track
next_track_row = self._find_next_track_row()
next_track_id = self._set_next(next_track_row)
self._repaint()
return next_track_id
@ -407,9 +410,9 @@ class PlaylistTab(QTableWidget):
# Called when we change tabs
self._repaint()
def select_next_track(self):
def select_next_row(self):
"""
Select next or first track. Don't select notes. Wrap at last row.
Select next or first row. Don't select notes. Wrap at last row.
"""
selected_rows = [row for row in
@ -454,7 +457,7 @@ class PlaylistTab(QTableWidget):
# Reset extended selection
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def select_previous_track(self):
def select_previous_row(self):
"""
Select previous or last track. Don't select notes. Wrap at first row.
"""
@ -514,7 +517,8 @@ class PlaylistTab(QTableWidget):
if not self.selectionModel().hasSelection():
return
return self._set_next(self.currentRow())
self._set_next(self.currentRow())
self._repaint()
# ########## Internally called functions ##########
@ -530,6 +534,11 @@ class PlaylistTab(QTableWidget):
duration = Tracks.get_duration(session, self._get_row_id(row))
return start + timedelta(milliseconds=duration)
def _can_read_track(self, track):
"Check track file is readable"
return os.access(track.path, os.R_OK)
def _context_menu(self, pos):
self.menu.exec_(self.mapToGlobal(pos))
@ -540,12 +549,10 @@ class PlaylistTab(QTableWidget):
DEBUG(f"playlist._delete_row({row})")
if row == self._meta_get_current():
# TODO
DEBUG("playlist._delete_row(): Can't delete playing track")
show_warning("Silly", "Can't delete playing track")
return
elif row == self._meta_get_next():
# TODO
DEBUG("playlist._delete_row(): Can't delete next track")
show_warning("Safety", "Can't delete next track")
return
with Session() as session:
@ -617,12 +624,12 @@ class PlaylistTab(QTableWidget):
if not track:
txt = f"Track not found (track.id={id})"
else:
txt = f"""
Title: {track.title}
Artist: {track.artist}
Path: {track.path}
Track ID: {track.id}
"""
txt = (
f"Title: {track.title}\n"
f"Artist: {track.artist}\n"
f"Path: {track.path}\n"
f"Track ID: {track.id}"
)
info = QMessageBox(self)
info.setIcon(QMessageBox.Information)
info.setText(txt)
@ -643,13 +650,13 @@ Track ID: {track.id}
and pos.y() >= rect.center().y() # noqa W503
)
def _mark_next_track(self):
def _find_next_track_row(self):
"""
Find next track to play.
If not found, return None.
If found, mark row with metadata and return track_id.
If found, return row number.
"""
found_next_track = False
@ -662,16 +669,14 @@ Track ID: {track.id}
for row in range(start, self.rowCount()):
if row in notes_rows:
continue
self._meta_set_next(row)
found_next_track = True
break
if not found_next_track:
if found_next_track:
return row
else:
return None
track_id = self._get_row_id(row)
return track_id
def _meta_clear(self, row):
"Clear metadata for row"
@ -808,11 +813,12 @@ Track ID: {track.id}
track_id = self._get_row_id(row)
if track_id:
if self._track_path_is_readable(track_id):
self._meta_set_next(self.currentRow())
self._meta_set_next(row)
self.master_process.set_next_track(track_id)
else:
self._meta_set_unreadable(self.currentRow())
self._repaint()
self._meta_set_unreadable(row)
track_id = None
return track_id
def _repaint(self, clear_selection=True):
"Set row colours, fonts, etc"

View File

@ -7,7 +7,7 @@ import tempfile
from config import Config
from log import DEBUG, INFO
from model import Tracks, Playdates, PlaylistTracks, Session
from model import Notes, Playdates, PlaylistTracks, Session, Tracks
from mutagen.flac import FLAC
from mutagen.mp3 import MP3
from pydub import AudioSegment, effects
@ -115,10 +115,75 @@ def create_track_from_file(session, path):
return track
def full_update_db():
def full_update_db(session):
"Rescan all entries in database"
print("Full scan not yet implemented")
def log(msg):
INFO(f"full_update_db(): {msg}")
def check_change(track_id, title, attribute, old, new):
if new > (old * 1.1) or new < (old * 0.9):
log(
"\n"
f"track[{track_id}] ({title}) "
f"{attribute} updated from {old} to {new}"
)
# Start with normal update to add new tracks and remove any missing
# files
log("update_db()")
update_db(session)
# Now update track length, silence and fade for every track in
# database
tracks = Tracks.get_all_tracks(session)
total_tracks = len(tracks)
log(f"Processing {total_tracks} tracks")
track_count = 0
for track in tracks:
track_count += 1
print(f"\rTrack {track_count} of {total_tracks}", end='')
# Sanity check
tag = get_music_info(track.path)
if not tag['title']:
log(f"track[{track.id}] {track.title=}: No tag title")
continue
if not tag['artist']:
log(f"track[{track.id}] {track.artist=}: No tag artist")
continue
# Update title and artist
if track.title != tag['title']:
track.title = tag['title']
if track.artist != tag['artist']:
track.artist = tag['artist']
# Update numbers; log if more than 10% different
duration = int(round(
tag['duration'], Config.MILLISECOND_SIGFIGS) * 1000)
check_change(track.id, track.title, "duration", track.duration,
duration)
track.duration = duration
audio = get_audio_segment(track.path)
start_gap = leading_silence(audio)
check_change(track.id, track.title, "start_gap", track.start_gap,
start_gap)
track.start_gap = start_gap
fade_at = fade_point(audio)
check_change(track.id, track.title, "fade_at", track.fade_at,
fade_at)
track.fade_at = fade_at
silence_at = trailing_silence(audio)
check_change(track.id, track.title, "silence_at", track.silence_at,
silence_at)
track.silence_at = silence_at
session.commit()
def get_audio_segment(path):
@ -168,11 +233,11 @@ def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE,
return min(trim_ms, len(audio_segment))
def fade_point(audio_segment, fade_threshold=Config.DBFS_FADE,
def fade_point(audio_segment, fade_threshold=0,
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
"""
Returns the millisecond/index of the point where the fade is down to
fade_threshold and doesn't get louder again.
Returns the millisecond/index of the point where the volume drops below
the maximum and doesn't get louder again.
audio_segment - the sdlg_search_database_uiegment to find silence in
fade_threshold - the upper bound for how quiet is silent in dFBS
chunk_size - chunk size for interating over the segment in ms
@ -182,6 +247,10 @@ def fade_point(audio_segment, fade_threshold=Config.DBFS_FADE,
segment_length = audio_segment.duration_seconds * 1000 # ms
trim_ms = segment_length - chunk_size
max_vol = audio_segment.dBFS
if fade_threshold == 0:
fade_threshold = max_vol
while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
and trim_ms > 0): # noqa W503
@ -192,22 +261,6 @@ def fade_point(audio_segment, fade_threshold=Config.DBFS_FADE,
return int(trim_ms)
# Current unused (1 June 2021)
# def rescan_database(session):
#
# tracks = Tracks.get_all_tracks(session)
# total_tracks = len(tracks)
# track_count = 0
# for track in tracks:
# track_count += 1
# print(f"Track {track_count} of {total_tracks}")
# audio = get_audio_segment(track.path)
# track.start_gap = leading_silence(audio)
# track.fade_at = fade_point(audio)
# track.silence_at = trailing_silence(audio)
# session.commit()
def trailing_silence(audio_segment, silence_threshold=-50.0,
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
return fade_point(audio_segment, silence_threshold, chunk_size)
@ -241,7 +294,7 @@ def update_db(session):
# is filename in database?
track = Tracks.get_track_from_filename(session, os.path.basename(path))
if not track:
DEBUG(f"songdb.update_db: Adding to database: {path}")
INFO(f"songdb.update_db: Adding to database: {path}")
create_track_from_file(session, path)
else:
# Check track info matches found track
@ -255,23 +308,26 @@ def update_db(session):
db_paths = set(Tracks.get_all_paths(session))
# Remote any tracks from database whose paths don't exist
for path in list(db_paths - os_paths):
# Manage tracks listed in database but where path is invalid
track = Tracks.get_track_from_path(session, path)
DEBUG(f"songdb.update_db(): remove from database: {path=} {track=}")
played = Playdates.last_played(session, track)
playlists = PlaylistTracks.get_playlists_containing_track_id(
session, track.id)
if played:
INFO(
f"songdb.update_db: Can't remove {track.id=} ({track.path=}) "
f"as it's in playdates.id={played.id}"
)
elif playlists:
INFO(
f"songdb.update_db: Can't remove {track.id=} ({track.path=} "
f"as it's in playlists {[p.name for p in playlists]}"
)
else:
Tracks.remove_path(session, path)
INFO(f"songdb.update_db(): remove from database: {path=} {track=}")
# Remove references from Playdates
Playdates.remove_track(session, track.id)
# Replace playlist entries with a note
note_txt = (
f"File removed: {track.title=}, {track.artist=}, "
f"{track.path=}"
)
for pt in PlaylistTracks.get_track_playlists(session, track.id):
# Create note
Notes.add_note(session, pt.playlist_id, pt.row, note_txt)
# Remove playlist entry
PlaylistTracks.remove_track(session, pt.playlist_id, pt.row)
# Remove Track entry pointing to invalid path
Tracks.remove_path(session, path)
if __name__ == '__main__' and '__file__' in globals():

View File

@ -149,6 +149,9 @@ border: 1px solid rgb(85, 87, 83);</string>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
@ -166,10 +169,25 @@ border: 1px solid rgb(85, 87, 83);</string>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="hdrNextTrack">
<widget class="ElideLabel" name="hdrNextTrack">
<property name="minimumSize">
<size>
<width>0</width>
<height>39</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>39</height>
</size>
</property>
<property name="font">
<font>
<family>Sans</family>
@ -183,6 +201,9 @@ border: 1px solid rgb(85, 87, 83);</string>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
@ -392,13 +413,13 @@ border: 1px solid rgb(85, 87, 83);</string>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnFade">
<widget class="QPushButton" name="btnStop">
<property name="text">
<string>Fade</string>
<string>Stop</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
<normaloff>:/icons/stopsign</normaloff>:/icons/stopsign</iconset>
</property>
<property name="iconSize">
<size>
@ -409,13 +430,13 @@ border: 1px solid rgb(85, 87, 83);</string>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnStop">
<widget class="QPushButton" name="btnFade">
<property name="text">
<string>Stop</string>
<string>Fade</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/stopsign</normaloff>:/icons/stopsign</iconset>
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
</property>
<property name="iconSize">
<size>
@ -984,6 +1005,13 @@ border: 1px solid rgb(85, 87, 83);</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ElideLabel</class>
<extends>QLabel</extends>
<header>musicmuster</header>
</customwidget>
</customwidgets>
<resources>
<include location="icons.qrc"/>
</resources>

View File

@ -85,6 +85,7 @@ class Ui_MainWindow(object):
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(True)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QLabel(self.centralwidget)
@ -95,9 +96,12 @@ class Ui_MainWindow(object):
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setWordWrap(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QLabel(self.centralwidget)
self.hdrNextTrack = ElideLabel(self.centralwidget)
self.hdrNextTrack.setMinimumSize(QtCore.QSize(0, 39))
self.hdrNextTrack.setMaximumSize(QtCore.QSize(16777215, 39))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
@ -105,6 +109,7 @@ class Ui_MainWindow(object):
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrNextTrack.setText("")
self.hdrNextTrack.setWordWrap(True)
self.hdrNextTrack.setObjectName("hdrNextTrack")
self.verticalLayout.addWidget(self.hdrNextTrack)
self.horizontalLayout_3.addLayout(self.verticalLayout)
@ -181,20 +186,20 @@ class Ui_MainWindow(object):
self.horizontalLayout.addWidget(self.btnSetNext)
spacerItem3 = QtWidgets.QSpacerItem(69, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem3)
self.btnFade = QtWidgets.QPushButton(self.frame_5)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnFade.setIcon(icon6)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.horizontalLayout.addWidget(self.btnFade)
self.btnStop = QtWidgets.QPushButton(self.frame_5)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnStop.setIcon(icon7)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnStop.setIcon(icon6)
self.btnStop.setIconSize(QtCore.QSize(30, 30))
self.btnStop.setObjectName("btnStop")
self.horizontalLayout.addWidget(self.btnStop)
self.btnFade = QtWidgets.QPushButton(self.frame_5)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnFade.setIcon(icon7)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.horizontalLayout.addWidget(self.btnFade)
self.spnVolume = QtWidgets.QSpinBox(self.frame_5)
self.spnVolume.setMaximum(100)
self.spnVolume.setProperty("value", 100)
@ -486,8 +491,8 @@ class Ui_MainWindow(object):
self.btnAddFile.setText(_translate("MainWindow", "Add file"))
self.btnAddNote.setText(_translate("MainWindow", "Add note"))
self.btnSetNext.setText(_translate("MainWindow", "Set next"))
self.btnFade.setText(_translate("MainWindow", "Fade"))
self.btnStop.setText(_translate("MainWindow", "Stop"))
self.btnFade.setText(_translate("MainWindow", "Fade"))
self.label_2.setText(_translate("MainWindow", "Started at:"))
self.label_start_tod.setText(_translate("MainWindow", "00:00:00"))
self.label_3.setText(_translate("MainWindow", "Silent at:"))
@ -540,4 +545,5 @@ class Ui_MainWindow(object):
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks"))
from musicmuster import ElideLabel
import icons_rc

45
archive/play.py Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python
from pydub import AudioSegment
def fade_point(audio_segment, fade_threshold=-12, chunk_size=10):
"""
Returns the millisecond/index of the point where the fade is down to
fade_threshold and doesn't get louder again.
audio_segment - the sdlg_search_database_uiegment to find silence in
fade_threshold - the upper bound for how quiet is silent in dFBS
chunk_size - chunk size for interating over the segment in ms
"""
assert chunk_size > 0 # to avoid infinite loop
segment_length = audio_segment.duration_seconds * 1000 # ms
print(f"segment_length={int(segment_length/1000)}")
trim_ms = segment_length - chunk_size
max_vol = audio_segment.dBFS
print(f"{max_vol=}")
fade_threshold = max_vol
while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
and trim_ms > 0): # noqa W503
trim_ms -= chunk_size
# if there is no trailing silence, return lenght of track (it's less
# the chunk_size, but for chunk_size = 10ms, this may be ignored)
print(f"Fade last {int(segment_length - trim_ms)/1000} seconds")
# print("Shout:")
# segment = AudioSegment.from_mp3("../archive/shout.mp3")
# fade_point(segment)
# print("Champagne:")
# segment = AudioSegment.from_mp3("../archive/champ.mp3")
# fade_point(segment)
# print("Be good:")
# segment = AudioSegment.from_mp3("../archive/wibg.mp3")
# fade_point(segment)
print("Be good:")
segment = AudioSegment.from_file("/tmp/bia.flac", "flac")
fade_point(segment)