Compare commits
19 Commits
94e7508a24
...
0e3e30391b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e3e30391b | ||
|
|
246b0d4915 | ||
|
|
fcf4ba3eb9 | ||
|
|
a7d9252619 | ||
|
|
d4f542cc29 | ||
|
|
2c9f041838 | ||
|
|
90a8209551 | ||
|
|
c0752407b9 | ||
|
|
87fb74b14f | ||
|
|
6336eb9215 | ||
|
|
ee74deaa49 | ||
|
|
00cae6dc52 | ||
|
|
11e3536801 | ||
|
|
427afee8da | ||
|
|
b4da349a8c | ||
|
|
0836f74d17 | ||
|
|
89d49f3e34 | ||
|
|
e813a01e14 | ||
|
|
72e3ef69ff |
@ -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):
|
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:
|
if not past_date:
|
||||||
@ -19,7 +26,21 @@ def get_relative_date(past_date, reference_date=None):
|
|||||||
if weeks == days == 0:
|
if weeks == days == 0:
|
||||||
# Played today, so return time instead
|
# Played today, so return time instead
|
||||||
return past_date.strftime("%H:%M")
|
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):
|
def ms_to_mmss(ms, decimals=0, negative=False):
|
||||||
|
|||||||
122
app/model.py
122
app/model.py
@ -53,6 +53,8 @@ class Notes(Base):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_note(session, playlist_id, row, text):
|
def add_note(session, playlist_id, row, text):
|
||||||
|
"Add note"
|
||||||
|
|
||||||
DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})")
|
DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})")
|
||||||
note = Notes()
|
note = Notes()
|
||||||
note.playlist_id = playlist_id
|
note.playlist_id = playlist_id
|
||||||
@ -64,16 +66,13 @@ class Notes(Base):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_note(session, id):
|
def delete_note(session, id):
|
||||||
|
"Delete note"
|
||||||
|
|
||||||
DEBUG(f"delete_note(id={id}")
|
DEBUG(f"delete_note(id={id}")
|
||||||
|
|
||||||
session.query(Notes).filter(Notes.id == id).delete()
|
session.query(Notes).filter(Notes.id == id).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# Not currently used 1 June 2021
|
|
||||||
# @staticmethod
|
|
||||||
# def get_note(session, id):
|
|
||||||
# return session.query(Notes).filter(Notes.id == id).one()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_note(cls, session, id, row, text=None):
|
def update_note(cls, session, id, row, text=None):
|
||||||
"""
|
"""
|
||||||
@ -99,6 +98,8 @@ class Playdates(Base):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_playdate(session, track):
|
def add_playdate(session, track):
|
||||||
|
"Record that track was played"
|
||||||
|
|
||||||
DEBUG(f"add_playdate(track={track})")
|
DEBUG(f"add_playdate(track={track})")
|
||||||
pd = Playdates()
|
pd = Playdates()
|
||||||
pd.lastplayed = datetime.now()
|
pd.lastplayed = datetime.now()
|
||||||
@ -119,6 +120,17 @@ class Playdates(Base):
|
|||||||
else:
|
else:
|
||||||
return None
|
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):
|
class Playlists(Base):
|
||||||
"""
|
"""
|
||||||
@ -293,18 +305,11 @@ class PlaylistTracks(Base):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@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 = []
|
return session.query(PlaylistTracks).filter(
|
||||||
playlist_ids = session.query(PlaylistTracks.playlist_id).filter(
|
|
||||||
PlaylistTracks.track_id == track_id).all()
|
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
|
@staticmethod
|
||||||
def move_track(session, from_playlist_id, row, to_playlist_id):
|
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"<Track(id={self.id}, title={self.title}, "
|
||||||
f"artist={self.artist}, path={self.path}>"
|
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
|
@classmethod
|
||||||
def get_or_create(cls, session, path):
|
def get_or_create(cls, session, path):
|
||||||
@ -468,30 +489,6 @@ class Tracks(Base):
|
|||||||
ERROR(f"Can't find track id {id}")
|
ERROR(f"Can't find track id {id}")
|
||||||
return None
|
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
|
@staticmethod
|
||||||
def get_track_from_filename(session, filename):
|
def get_track_from_filename(session, filename):
|
||||||
"""
|
"""
|
||||||
@ -519,6 +516,29 @@ class Tracks(Base):
|
|||||||
|
|
||||||
return session.query(Tracks).filter(Tracks.path == path).first()
|
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
|
@staticmethod
|
||||||
def remove_path(session, path):
|
def remove_path(session, path):
|
||||||
"Remove track with passed path from database"
|
"Remove track with passed path from database"
|
||||||
@ -527,9 +547,33 @@ class Tracks(Base):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
session.query(Tracks).filter(Tracks.path == path).delete()
|
session.query(Tracks).filter(Tracks.path == path).delete()
|
||||||
|
session.commit()
|
||||||
except IntegrityError as exception:
|
except IntegrityError as exception:
|
||||||
ERROR(f"Can't remove track with {path=} ({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
|
@staticmethod
|
||||||
def search_titles(session, text):
|
def search_titles(session, text):
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -8,17 +8,17 @@ import urllib.parse
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from log import DEBUG, EXCEPTION
|
from log import DEBUG, EXCEPTION
|
||||||
from slugify import slugify
|
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
from PyQt5.QtGui import QFontMetrics, QPainter
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QDialog,
|
QDialog,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QInputDialog,
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMessageBox,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import helpers
|
import helpers
|
||||||
@ -34,6 +34,21 @@ from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
|
|||||||
from ui.main_window_ui import Ui_MainWindow
|
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):
|
class Window(QMainWindow, Ui_MainWindow):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -139,10 +154,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
|
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
|
||||||
self.actionPlay_next.triggered.connect(self.play_next)
|
self.actionPlay_next.triggered.connect(self.play_next)
|
||||||
self.actionSearch_database.triggered.connect(self.search_database)
|
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_played_tracks.triggered.connect(self.select_played)
|
||||||
self.actionSelect_previous_track.triggered.connect(
|
self.actionSelect_previous_track.triggered.connect(
|
||||||
self.select_previous_track)
|
self.select_previous_row)
|
||||||
self.actionSelect_unplayed_tracks.triggered.connect(
|
self.actionSelect_unplayed_tracks.triggered.connect(
|
||||||
self.select_unplayed)
|
self.select_unplayed)
|
||||||
self.actionSetNext.triggered.connect(self.set_next_track)
|
self.actionSetNext.triggered.connect(self.set_next_track)
|
||||||
@ -224,13 +239,17 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def end_of_track_actions(self):
|
def end_of_track_actions(self):
|
||||||
"Clean up after track played"
|
"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
|
# Clean up metadata
|
||||||
self.previous_track = self.current_track
|
self.previous_track = self.current_track
|
||||||
self.current_track_playlist_tab.play_stopped()
|
if self.current_track_playlist_tab:
|
||||||
self.current_track_playlist_tab.clear_current()
|
self.current_track_playlist_tab.play_stopped()
|
||||||
self.current_track_playlist_tab = None
|
self.current_track_playlist_tab.clear_current()
|
||||||
|
self.current_track_playlist_tab = None
|
||||||
self.current_track = None
|
self.current_track = None
|
||||||
self.playing = False
|
|
||||||
|
|
||||||
# Clean up display
|
# Clean up display
|
||||||
self.label_end_timer.setText("00:00")
|
self.label_end_timer.setText("00:00")
|
||||||
@ -238,6 +257,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.frame_fade.setStyleSheet("")
|
self.frame_fade.setStyleSheet("")
|
||||||
self.update_headers()
|
self.update_headers()
|
||||||
|
|
||||||
|
# Enable controls
|
||||||
|
self.enable_play_next_controls()
|
||||||
|
|
||||||
def export_playlist_tab(self):
|
def export_playlist_tab(self):
|
||||||
"Export the current playlist to an m3u file"
|
"Export the current playlist to an m3u file"
|
||||||
|
|
||||||
@ -284,10 +306,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if not self.current_track:
|
if not self.current_track:
|
||||||
return
|
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.previous_track_position = self.music.fade()
|
||||||
self.end_of_track_actions()
|
self.end_of_track_actions()
|
||||||
|
|
||||||
@ -414,17 +432,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.music.play(self.current_track.path)
|
self.music.play(self.current_track.path)
|
||||||
|
|
||||||
# Update metadata
|
# 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()
|
next_track_id = self.current_track_playlist_tab.play_started()
|
||||||
|
|
||||||
if next_track_id is not None:
|
if next_track_id is not None:
|
||||||
self.next_track = Tracks.get_track(session, next_track_id)
|
self.next_track = Tracks.get_track(session, next_track_id)
|
||||||
self.next_track_playlist_tab = self.current_track_playlist_tab
|
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:
|
else:
|
||||||
self.next_track = self.next_track_playlist_tab = None
|
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)
|
playlist_db = Playlists.open(session, dlg.plid)
|
||||||
self.load_playlist(session, playlist_db)
|
self.load_playlist(session, playlist_db)
|
||||||
|
|
||||||
def select_next_track(self):
|
def select_next_row(self):
|
||||||
"Select next or first track in playlist"
|
"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):
|
def select_played(self):
|
||||||
"Select all played tracks in playlist"
|
"Select all played tracks in playlist"
|
||||||
|
|
||||||
self.visible_playlist_tab().select_played_tracks()
|
self.visible_playlist_tab().select_played_tracks()
|
||||||
|
|
||||||
def select_previous_track(self):
|
def select_previous_row(self):
|
||||||
"Select previous or first track in playlist"
|
"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):
|
def set_next_track(self, next_track_id=None):
|
||||||
"Set selected track as next"
|
"Set selected track as next"
|
||||||
@ -500,11 +516,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
self.visible_playlist_tab().select_unplayed_tracks()
|
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):
|
def song_info_search(self):
|
||||||
"""
|
"""
|
||||||
Open browser tabs for Wikipedia, searching for
|
Open browser tabs for Wikipedia, searching for
|
||||||
@ -533,15 +544,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
DEBUG("musicmuster.stop()")
|
DEBUG("musicmuster.stop()")
|
||||||
|
|
||||||
self.stop_playing(fade=False)
|
self.stop_playing(fade=False)
|
||||||
self.enable_play_next_controls()
|
|
||||||
|
|
||||||
def stop_playing(self, fade=True):
|
def stop_playing(self, fade=True):
|
||||||
"Stop playing current track"
|
"Stop playing current track"
|
||||||
|
|
||||||
DEBUG("musicmuster.stop_playing()", True)
|
DEBUG(f"musicmuster.stop_playing({fade=})", True)
|
||||||
|
|
||||||
if not self.music.playing():
|
if not self.music.playing():
|
||||||
DEBUG("musicmuster.stop_playing(): not playing", True)
|
DEBUG("musicmuster.stop_playing(): not playing", True)
|
||||||
|
self.end_of_track_actions()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.previous_track_position = self.music.get_position()
|
self.previous_track_position = self.music.get_position()
|
||||||
@ -549,8 +560,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
DEBUG("musicmuster.stop_playing(): fading music", True)
|
DEBUG("musicmuster.stop_playing(): fading music", True)
|
||||||
self.music.fade()
|
self.music.fade()
|
||||||
else:
|
else:
|
||||||
self.music.stop()
|
|
||||||
DEBUG("musicmuster.stop_playing(): stopping music", True)
|
DEBUG("musicmuster.stop_playing(): stopping music", True)
|
||||||
|
self.music.stop()
|
||||||
|
|
||||||
self.end_of_track_actions()
|
self.end_of_track_actions()
|
||||||
|
|
||||||
@ -618,16 +629,20 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Time to fade
|
# Time to fade
|
||||||
self.label_fade_timer.setText(helpers.ms_to_mmss(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:
|
if time_to_silence <= 5500:
|
||||||
self.frame_silent.setStyleSheet(
|
self.frame_silent.setStyleSheet(
|
||||||
f"background: {Config.COLOUR_ENDING_TIMER}"
|
f"background: {Config.COLOUR_ENDING_TIMER}"
|
||||||
)
|
)
|
||||||
self.enable_play_next_controls()
|
self.enable_play_next_controls()
|
||||||
|
# Set warning colour on time to silence box when fade starts
|
||||||
elif time_to_fade <= 500:
|
elif time_to_fade <= 500:
|
||||||
self.frame_silent.setStyleSheet(
|
self.frame_silent.setStyleSheet(
|
||||||
f"background: {Config.COLOUR_WARNING_TIMER}"
|
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:
|
elif time_to_fade <= 5500:
|
||||||
self.frame_fade.setStyleSheet(
|
self.frame_fade.setStyleSheet(
|
||||||
f"background: {Config.COLOUR_WARNING_TIMER}"
|
f"background: {Config.COLOUR_WARNING_TIMER}"
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import os
|
|||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from datetime import datetime, timedelta
|
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 log import DEBUG, ERROR
|
||||||
from model import (
|
from model import (
|
||||||
Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks
|
Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks
|
||||||
@ -155,8 +155,8 @@ class PlaylistTab(QTableWidget):
|
|||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
act_delete = self.menu.addAction('Delete')
|
act_delete = self.menu.addAction('Delete')
|
||||||
act_delete.triggered.connect(lambda: self._delete_row(row))
|
act_delete.triggered.connect(lambda: self._delete_row(row))
|
||||||
act_delete = self.menu.addAction('Info')
|
act_info = self.menu.addAction('Info')
|
||||||
act_delete.triggered.connect(lambda: self._info_row(row))
|
act_info.triggered.connect(lambda: self._info_row(row))
|
||||||
|
|
||||||
return super(PlaylistTab, self).eventFilter(source, event)
|
return super(PlaylistTab, self).eventFilter(source, event)
|
||||||
|
|
||||||
@ -357,7 +357,10 @@ class PlaylistTab(QTableWidget):
|
|||||||
# Scroll to put current track in centre
|
# Scroll to put current track in centre
|
||||||
scroll_to = self.item(current_row, self.COL_INDEX)
|
scroll_to = self.item(current_row, self.COL_INDEX)
|
||||||
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter)
|
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()
|
self._repaint()
|
||||||
return next_track_id
|
return next_track_id
|
||||||
|
|
||||||
@ -407,9 +410,9 @@ class PlaylistTab(QTableWidget):
|
|||||||
# Called when we change tabs
|
# Called when we change tabs
|
||||||
self._repaint()
|
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
|
selected_rows = [row for row in
|
||||||
@ -454,7 +457,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
# Reset extended selection
|
# Reset extended selection
|
||||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
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.
|
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():
|
if not self.selectionModel().hasSelection():
|
||||||
return
|
return
|
||||||
|
|
||||||
return self._set_next(self.currentRow())
|
self._set_next(self.currentRow())
|
||||||
|
self._repaint()
|
||||||
|
|
||||||
# ########## Internally called functions ##########
|
# ########## Internally called functions ##########
|
||||||
|
|
||||||
@ -530,6 +534,11 @@ class PlaylistTab(QTableWidget):
|
|||||||
duration = Tracks.get_duration(session, self._get_row_id(row))
|
duration = Tracks.get_duration(session, self._get_row_id(row))
|
||||||
return start + timedelta(milliseconds=duration)
|
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):
|
def _context_menu(self, pos):
|
||||||
|
|
||||||
self.menu.exec_(self.mapToGlobal(pos))
|
self.menu.exec_(self.mapToGlobal(pos))
|
||||||
@ -540,12 +549,10 @@ class PlaylistTab(QTableWidget):
|
|||||||
DEBUG(f"playlist._delete_row({row})")
|
DEBUG(f"playlist._delete_row({row})")
|
||||||
|
|
||||||
if row == self._meta_get_current():
|
if row == self._meta_get_current():
|
||||||
# TODO
|
show_warning("Silly", "Can't delete playing track")
|
||||||
DEBUG("playlist._delete_row(): Can't delete playing track")
|
|
||||||
return
|
return
|
||||||
elif row == self._meta_get_next():
|
elif row == self._meta_get_next():
|
||||||
# TODO
|
show_warning("Safety", "Can't delete next track")
|
||||||
DEBUG("playlist._delete_row(): Can't delete next track")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
@ -617,12 +624,12 @@ class PlaylistTab(QTableWidget):
|
|||||||
if not track:
|
if not track:
|
||||||
txt = f"Track not found (track.id={id})"
|
txt = f"Track not found (track.id={id})"
|
||||||
else:
|
else:
|
||||||
txt = f"""
|
txt = (
|
||||||
Title: {track.title}
|
f"Title: {track.title}\n"
|
||||||
Artist: {track.artist}
|
f"Artist: {track.artist}\n"
|
||||||
Path: {track.path}
|
f"Path: {track.path}\n"
|
||||||
Track ID: {track.id}
|
f"Track ID: {track.id}"
|
||||||
"""
|
)
|
||||||
info = QMessageBox(self)
|
info = QMessageBox(self)
|
||||||
info.setIcon(QMessageBox.Information)
|
info.setIcon(QMessageBox.Information)
|
||||||
info.setText(txt)
|
info.setText(txt)
|
||||||
@ -643,13 +650,13 @@ Track ID: {track.id}
|
|||||||
and pos.y() >= rect.center().y() # noqa W503
|
and pos.y() >= rect.center().y() # noqa W503
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mark_next_track(self):
|
def _find_next_track_row(self):
|
||||||
"""
|
"""
|
||||||
Find next track to play.
|
Find next track to play.
|
||||||
|
|
||||||
If not found, return None.
|
If not found, return None.
|
||||||
|
|
||||||
If found, mark row with metadata and return track_id.
|
If found, return row number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
found_next_track = False
|
found_next_track = False
|
||||||
@ -662,16 +669,14 @@ Track ID: {track.id}
|
|||||||
for row in range(start, self.rowCount()):
|
for row in range(start, self.rowCount()):
|
||||||
if row in notes_rows:
|
if row in notes_rows:
|
||||||
continue
|
continue
|
||||||
self._meta_set_next(row)
|
|
||||||
found_next_track = True
|
found_next_track = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not found_next_track:
|
if found_next_track:
|
||||||
|
return row
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
track_id = self._get_row_id(row)
|
|
||||||
return track_id
|
|
||||||
|
|
||||||
def _meta_clear(self, row):
|
def _meta_clear(self, row):
|
||||||
"Clear metadata for row"
|
"Clear metadata for row"
|
||||||
|
|
||||||
@ -808,11 +813,12 @@ Track ID: {track.id}
|
|||||||
track_id = self._get_row_id(row)
|
track_id = self._get_row_id(row)
|
||||||
if track_id:
|
if track_id:
|
||||||
if self._track_path_is_readable(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)
|
self.master_process.set_next_track(track_id)
|
||||||
else:
|
else:
|
||||||
self._meta_set_unreadable(self.currentRow())
|
self._meta_set_unreadable(row)
|
||||||
self._repaint()
|
track_id = None
|
||||||
|
return track_id
|
||||||
|
|
||||||
def _repaint(self, clear_selection=True):
|
def _repaint(self, clear_selection=True):
|
||||||
"Set row colours, fonts, etc"
|
"Set row colours, fonts, etc"
|
||||||
|
|||||||
134
app/songdb.py
134
app/songdb.py
@ -7,7 +7,7 @@ import tempfile
|
|||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from log import DEBUG, INFO
|
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.flac import FLAC
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from pydub import AudioSegment, effects
|
from pydub import AudioSegment, effects
|
||||||
@ -115,10 +115,75 @@ def create_track_from_file(session, path):
|
|||||||
return track
|
return track
|
||||||
|
|
||||||
|
|
||||||
def full_update_db():
|
def full_update_db(session):
|
||||||
"Rescan all entries in database"
|
"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):
|
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))
|
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):
|
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
||||||
"""
|
"""
|
||||||
Returns the millisecond/index of the point where the fade is down to
|
Returns the millisecond/index of the point where the volume drops below
|
||||||
fade_threshold and doesn't get louder again.
|
the maximum and doesn't get louder again.
|
||||||
audio_segment - the sdlg_search_database_uiegment to find silence in
|
audio_segment - the sdlg_search_database_uiegment to find silence in
|
||||||
fade_threshold - the upper bound for how quiet is silent in dFBS
|
fade_threshold - the upper bound for how quiet is silent in dFBS
|
||||||
chunk_size - chunk size for interating over the segment in ms
|
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
|
segment_length = audio_segment.duration_seconds * 1000 # ms
|
||||||
trim_ms = segment_length - chunk_size
|
trim_ms = segment_length - chunk_size
|
||||||
|
max_vol = audio_segment.dBFS
|
||||||
|
if fade_threshold == 0:
|
||||||
|
fade_threshold = max_vol
|
||||||
|
|
||||||
while (
|
while (
|
||||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
||||||
and trim_ms > 0): # noqa W503
|
and trim_ms > 0): # noqa W503
|
||||||
@ -192,22 +261,6 @@ def fade_point(audio_segment, fade_threshold=Config.DBFS_FADE,
|
|||||||
return int(trim_ms)
|
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,
|
def trailing_silence(audio_segment, silence_threshold=-50.0,
|
||||||
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
||||||
return fade_point(audio_segment, silence_threshold, chunk_size)
|
return fade_point(audio_segment, silence_threshold, chunk_size)
|
||||||
@ -241,7 +294,7 @@ def update_db(session):
|
|||||||
# is filename in database?
|
# is filename in database?
|
||||||
track = Tracks.get_track_from_filename(session, os.path.basename(path))
|
track = Tracks.get_track_from_filename(session, os.path.basename(path))
|
||||||
if not track:
|
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)
|
create_track_from_file(session, path)
|
||||||
else:
|
else:
|
||||||
# Check track info matches found track
|
# Check track info matches found track
|
||||||
@ -255,23 +308,26 @@ def update_db(session):
|
|||||||
db_paths = set(Tracks.get_all_paths(session))
|
db_paths = set(Tracks.get_all_paths(session))
|
||||||
# Remote any tracks from database whose paths don't exist
|
# Remote any tracks from database whose paths don't exist
|
||||||
for path in list(db_paths - os_paths):
|
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)
|
track = Tracks.get_track_from_path(session, path)
|
||||||
DEBUG(f"songdb.update_db(): remove from database: {path=} {track=}")
|
INFO(f"songdb.update_db(): remove from database: {path=} {track=}")
|
||||||
played = Playdates.last_played(session, track)
|
|
||||||
playlists = PlaylistTracks.get_playlists_containing_track_id(
|
# Remove references from Playdates
|
||||||
session, track.id)
|
Playdates.remove_track(session, track.id)
|
||||||
if played:
|
|
||||||
INFO(
|
# Replace playlist entries with a note
|
||||||
f"songdb.update_db: Can't remove {track.id=} ({track.path=}) "
|
note_txt = (
|
||||||
f"as it's in playdates.id={played.id}"
|
f"File removed: {track.title=}, {track.artist=}, "
|
||||||
)
|
f"{track.path=}"
|
||||||
elif playlists:
|
)
|
||||||
INFO(
|
for pt in PlaylistTracks.get_track_playlists(session, track.id):
|
||||||
f"songdb.update_db: Can't remove {track.id=} ({track.path=} "
|
# Create note
|
||||||
f"as it's in playlists {[p.name for p in playlists]}"
|
Notes.add_note(session, pt.playlist_id, pt.row, note_txt)
|
||||||
)
|
# Remove playlist entry
|
||||||
else:
|
PlaylistTracks.remove_track(session, pt.playlist_id, pt.row)
|
||||||
Tracks.remove_path(session, path)
|
|
||||||
|
# Remove Track entry pointing to invalid path
|
||||||
|
Tracks.remove_path(session, path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' and '__file__' in globals():
|
if __name__ == '__main__' and '__file__' in globals():
|
||||||
|
|||||||
@ -149,6 +149,9 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -166,10 +169,25 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<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">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<family>Sans</family>
|
<family>Sans</family>
|
||||||
@ -183,6 +201,9 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
@ -392,13 +413,13 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="btnFade">
|
<widget class="QPushButton" name="btnStop">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Fade</string>
|
<string>Stop</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="icons.qrc">
|
<iconset resource="icons.qrc">
|
||||||
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
|
<normaloff>:/icons/stopsign</normaloff>:/icons/stopsign</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="iconSize">
|
<property name="iconSize">
|
||||||
<size>
|
<size>
|
||||||
@ -409,13 +430,13 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="btnStop">
|
<widget class="QPushButton" name="btnFade">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Stop</string>
|
<string>Fade</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="icons.qrc">
|
<iconset resource="icons.qrc">
|
||||||
<normaloff>:/icons/stopsign</normaloff>:/icons/stopsign</iconset>
|
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="iconSize">
|
<property name="iconSize">
|
||||||
<size>
|
<size>
|
||||||
@ -984,6 +1005,13 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>ElideLabel</class>
|
||||||
|
<extends>QLabel</extends>
|
||||||
|
<header>musicmuster</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="icons.qrc"/>
|
<include location="icons.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -85,6 +85,7 @@ class Ui_MainWindow(object):
|
|||||||
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
|
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"border: 1px solid rgb(85, 87, 83);")
|
||||||
self.hdrPreviousTrack.setText("")
|
self.hdrPreviousTrack.setText("")
|
||||||
|
self.hdrPreviousTrack.setWordWrap(True)
|
||||||
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
|
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
|
||||||
self.verticalLayout.addWidget(self.hdrPreviousTrack)
|
self.verticalLayout.addWidget(self.hdrPreviousTrack)
|
||||||
self.hdrCurrentTrack = QtWidgets.QLabel(self.centralwidget)
|
self.hdrCurrentTrack = QtWidgets.QLabel(self.centralwidget)
|
||||||
@ -95,9 +96,12 @@ class Ui_MainWindow(object):
|
|||||||
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
|
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"border: 1px solid rgb(85, 87, 83);")
|
||||||
self.hdrCurrentTrack.setText("")
|
self.hdrCurrentTrack.setText("")
|
||||||
|
self.hdrCurrentTrack.setWordWrap(True)
|
||||||
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
||||||
self.verticalLayout.addWidget(self.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 = QtGui.QFont()
|
||||||
font.setFamily("Sans")
|
font.setFamily("Sans")
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
@ -105,6 +109,7 @@ class Ui_MainWindow(object):
|
|||||||
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
|
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"border: 1px solid rgb(85, 87, 83);")
|
||||||
self.hdrNextTrack.setText("")
|
self.hdrNextTrack.setText("")
|
||||||
|
self.hdrNextTrack.setWordWrap(True)
|
||||||
self.hdrNextTrack.setObjectName("hdrNextTrack")
|
self.hdrNextTrack.setObjectName("hdrNextTrack")
|
||||||
self.verticalLayout.addWidget(self.hdrNextTrack)
|
self.verticalLayout.addWidget(self.hdrNextTrack)
|
||||||
self.horizontalLayout_3.addLayout(self.verticalLayout)
|
self.horizontalLayout_3.addLayout(self.verticalLayout)
|
||||||
@ -181,20 +186,20 @@ class Ui_MainWindow(object):
|
|||||||
self.horizontalLayout.addWidget(self.btnSetNext)
|
self.horizontalLayout.addWidget(self.btnSetNext)
|
||||||
spacerItem3 = QtWidgets.QSpacerItem(69, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
spacerItem3 = QtWidgets.QSpacerItem(69, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
self.horizontalLayout.addItem(spacerItem3)
|
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)
|
self.btnStop = QtWidgets.QPushButton(self.frame_5)
|
||||||
icon7 = QtGui.QIcon()
|
icon6 = QtGui.QIcon()
|
||||||
icon7.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
icon6.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
||||||
self.btnStop.setIcon(icon7)
|
self.btnStop.setIcon(icon6)
|
||||||
self.btnStop.setIconSize(QtCore.QSize(30, 30))
|
self.btnStop.setIconSize(QtCore.QSize(30, 30))
|
||||||
self.btnStop.setObjectName("btnStop")
|
self.btnStop.setObjectName("btnStop")
|
||||||
self.horizontalLayout.addWidget(self.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 = QtWidgets.QSpinBox(self.frame_5)
|
||||||
self.spnVolume.setMaximum(100)
|
self.spnVolume.setMaximum(100)
|
||||||
self.spnVolume.setProperty("value", 100)
|
self.spnVolume.setProperty("value", 100)
|
||||||
@ -486,8 +491,8 @@ class Ui_MainWindow(object):
|
|||||||
self.btnAddFile.setText(_translate("MainWindow", "Add file"))
|
self.btnAddFile.setText(_translate("MainWindow", "Add file"))
|
||||||
self.btnAddNote.setText(_translate("MainWindow", "Add note"))
|
self.btnAddNote.setText(_translate("MainWindow", "Add note"))
|
||||||
self.btnSetNext.setText(_translate("MainWindow", "Set next"))
|
self.btnSetNext.setText(_translate("MainWindow", "Set next"))
|
||||||
self.btnFade.setText(_translate("MainWindow", "Fade"))
|
|
||||||
self.btnStop.setText(_translate("MainWindow", "Stop"))
|
self.btnStop.setText(_translate("MainWindow", "Stop"))
|
||||||
|
self.btnFade.setText(_translate("MainWindow", "Fade"))
|
||||||
self.label_2.setText(_translate("MainWindow", "Started at:"))
|
self.label_2.setText(_translate("MainWindow", "Started at:"))
|
||||||
self.label_start_tod.setText(_translate("MainWindow", "00:00:00"))
|
self.label_start_tod.setText(_translate("MainWindow", "00:00:00"))
|
||||||
self.label_3.setText(_translate("MainWindow", "Silent at:"))
|
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_previous_track.setShortcut(_translate("MainWindow", "K"))
|
||||||
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
|
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
|
||||||
self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks"))
|
self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks"))
|
||||||
|
from musicmuster import ElideLabel
|
||||||
import icons_rc
|
import icons_rc
|
||||||
|
|||||||
45
archive/play.py
Executable file
45
archive/play.py
Executable 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)
|
||||||
Loading…
Reference in New Issue
Block a user