Compare commits

..

No commits in common. "6bf9330b629db0f927ac3683d76406ea53963fb4" and "dda74782b601e042832e2723e06418e702f90b01" have entirely different histories.

9 changed files with 571 additions and 594 deletions

View File

@ -10,6 +10,7 @@ class Config(object):
CART_DIRECTORY = "/home/kae/radio/CartTracks" CART_DIRECTORY = "/home/kae/radio/CartTracks"
CARTS_COUNT = 10 CARTS_COUNT = 10
CARTS_HIDE = True CARTS_HIDE = True
COLON_IN_PATH_FIX = True
COLOUR_BITRATE_LOW = "#ffcdd2" COLOUR_BITRATE_LOW = "#ffcdd2"
COLOUR_BITRATE_MEDIUM = "#ffeb6f" COLOUR_BITRATE_MEDIUM = "#ffeb6f"
COLOUR_BITRATE_OK = "#dcedc8" COLOUR_BITRATE_OK = "#dcedc8"
@ -49,7 +50,6 @@ class Config(object):
ERRORS_TO = ['kae@midnighthax.com'] ERRORS_TO = ['kae@midnighthax.com']
FADE_STEPS = 20 FADE_STEPS = 20
FADE_TIME = 3000 FADE_TIME = 3000
HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"
LOG_LEVEL_STDERR = logging.ERROR LOG_LEVEL_STDERR = logging.ERROR
@ -67,7 +67,6 @@ class Config(object):
MINIMUM_ROW_HEIGHT = 30 MINIMUM_ROW_HEIGHT = 30
MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501 MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501
NOTE_TIME_FORMAT = "%H:%M:%S" NOTE_TIME_FORMAT = "%H:%M:%S"
PLAY_SETTLE = 500000
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") IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
SCROLL_TOP_MARGIN = 3 SCROLL_TOP_MARGIN = 3

View File

@ -46,10 +46,7 @@ def Session() -> Generator[scoped_session, None, None]:
lineno = frame.lineno lineno = frame.lineno
Session = scoped_session(sessionmaker(bind=engine, future=True)) Session = scoped_session(sessionmaker(bind=engine, future=True))
log.debug(f"SqlA: session acquired [{hex(id(Session))}]") log.debug(f"SqlA: session acquired [{hex(id(Session))}]")
log.debug( log.debug(f"Session acquisition: {function}:{lineno} [{hex(id(Session))}]")
f"Session acquisition: {file}:{function}:{lineno} "
f"[{hex(id(Session))}]"
)
yield Session yield Session
log.debug(f" SqlA: session released [{hex(id(Session))}]") log.debug(f" SqlA: session released [{hex(id(Session))}]")
Session.commit() Session.commit()

View File

@ -55,15 +55,15 @@ def fade_point(
return int(trim_ms) return int(trim_ms)
def file_is_unreadable(path: Optional[str]) -> bool: def file_is_readable(path: Optional[str]) -> bool:
""" """
Returns True if passed path is readable, else False Returns True if passed path is readable, else False
""" """
if not path: if not path:
return True return False
return not os.access(path, os.R_OK) return os.access(path, os.R_OK)
def get_audio_segment(path: str) -> Optional[AudioSegment]: def get_audio_segment(path: str) -> Optional[AudioSegment]:

View File

@ -20,7 +20,6 @@ class InfoTabs(QTabWidget):
# Dictionary to record when tabs were last updated (so we can # Dictionary to record when tabs were last updated (so we can
# re-use the oldest one later) # re-use the oldest one later)
self.last_update: Dict[QWebEngineView, datetime] = {} self.last_update: Dict[QWebEngineView, datetime] = {}
self.tabtitles: Dict[int, str] = {}
def open_in_songfacts(self, title): def open_in_songfacts(self, title):
"""Search Songfacts for title""" """Search Songfacts for title"""
@ -40,19 +39,10 @@ class InfoTabs(QTabWidget):
def open_tab(self, url: str, title: str) -> None: def open_tab(self, url: str, title: str) -> None:
""" """
Open passed URL. If URL currently displayed, switch to that tab. Open passed URL. Create new tab if we're below the maximum
Create new tab if we're below the maximum
number otherwise reuse oldest content tab. number otherwise reuse oldest content tab.
""" """
if url in self.tabtitles.values():
self.setCurrentIndex(
list(self.tabtitles.keys())[
list(self.tabtitles.values()).index(url)
]
)
return
short_title = title[:Config.INFO_TAB_TITLE_LENGTH] short_title = title[:Config.INFO_TAB_TITLE_LENGTH]
if self.count() < Config.MAX_INFO_TABS: if self.count() < Config.MAX_INFO_TABS:
@ -71,7 +61,6 @@ class InfoTabs(QTabWidget):
widget.setUrl(QUrl(url)) widget.setUrl(QUrl(url))
self.last_update[widget] = datetime.now() self.last_update[widget] = datetime.now()
self.tabtitles[tab_index] = url
# Show newly updated tab # Show newly updated tab
self.setCurrentIndex(tab_index) self.setCurrentIndex(tab_index)

View File

@ -75,8 +75,7 @@ def log_uncaught_exceptions(_ex_cls, ex, tb):
print("\033[1;31;47m") print("\033[1;31;47m")
logging.critical(''.join(traceback.format_tb(tb))) logging.critical(''.join(traceback.format_tb(tb)))
print("\033[1;37;40m") print("\033[1;37;40m")
print(stackprinter.format(ex, show_vals="all", add_summary=True, print(stackprinter.format(ex, style="darkbg2", add_summary=True))
style="darkbg"))
if os.environ["MM_ENV"] == "PRODUCTION": if os.environ["MM_ENV"] == "PRODUCTION":
msg = stackprinter.format(ex) msg = stackprinter.format(ex)
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,

View File

@ -98,13 +98,13 @@ class NoteColours(Base):
) )
@staticmethod @staticmethod
def get_colour(session: scoped_session, text: str) -> Optional[str]: def get_colour(session: scoped_session, text: str) -> str:
""" """
Parse text and return colour string if matched, else empty string Parse text and return colour string if matched, else empty string
""" """
if not text: if not text:
return None return ""
for rec in session.execute( for rec in session.execute(
select(NoteColours) select(NoteColours)
@ -126,7 +126,7 @@ class NoteColours(Base):
if rec.substring.lower() in text.lower(): if rec.substring.lower() in text.lower():
return rec.colour return rec.colour
return None return ""
class Playdates(Base): class Playdates(Base):
@ -464,32 +464,26 @@ class PlaylistRows(Base):
session.commit() session.commit()
@classmethod @classmethod
def get_from_id_list(cls, session: scoped_session, playlist_id: int, def get_section_header_rows(cls, session: scoped_session,
plr_ids: List[int]) -> List["PlaylistRows"]: playlist_id: int) -> List["PlaylistRows"]:
""" """
Take a list of PlaylistRows ids and return a list of corresponding Return a list of PlaylistRows that are section headers for this
PlaylistRows objects playlist
""" """
plrs = session.execute( plrs = session.execute(
select(cls) select(cls)
.where( .where(
cls.playlist_id == playlist_id, cls.playlist_id == playlist_id,
cls.id.in_(plr_ids) cls.track_id.is_(None),
(
cls.note.endswith("-") |
cls.note.endswith("+")
)
).order_by(cls.row_number)).scalars().all() ).order_by(cls.row_number)).scalars().all()
return plrs return plrs
@staticmethod
def get_last_used_row(session: scoped_session,
playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows"""
return session.execute(
select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == playlist_id)
).scalar_one()
@staticmethod @staticmethod
def get_track_plr(session: scoped_session, track_id: int, def get_track_plr(session: scoped_session, track_id: int,
playlist_id: int) -> Optional["PlaylistRows"]: playlist_id: int) -> Optional["PlaylistRows"]:
@ -504,6 +498,16 @@ class PlaylistRows(Base):
.limit(1) .limit(1)
).first() ).first()
@staticmethod
def get_last_used_row(session: scoped_session,
playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows"""
return session.execute(
select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == playlist_id)
).scalar_one()
@classmethod @classmethod
def get_played_rows(cls, session: scoped_session, def get_played_rows(cls, session: scoped_session,
playlist_id: int) -> List["PlaylistRows"]: playlist_id: int) -> List["PlaylistRows"]:
@ -568,6 +572,27 @@ class PlaylistRows(Base):
return plrs return plrs
@staticmethod
def indexed_by_id(session: scoped_session,
plr_ids: Union[Iterable[int], ValuesView]) -> dict:
"""
Return a dictionary of playlist_rows indexed by their plr id from
the passed plr_id list.
"""
plrs = session.execute(
select(PlaylistRows)
.where(
PlaylistRows.id.in_(plr_ids)
)
).scalars().all()
result = {}
for plr in plrs:
result[plr.id] = plr
return result
@staticmethod @staticmethod
def move_rows_down(session: scoped_session, playlist_id: int, def move_rows_down(session: scoped_session, playlist_id: int,
starting_row: int, move_by: int) -> None: starting_row: int, move_by: int) -> None:

View File

@ -4,7 +4,7 @@ import vlc # type: ignore
# #
from config import Config from config import Config
from datetime import datetime from datetime import datetime
from helpers import file_is_unreadable from helpers import file_is_readable
from typing import Optional from typing import Optional
from time import sleep from time import sleep
@ -101,14 +101,17 @@ class Music:
Log and return if path not found. Log and return if path not found.
""" """
if file_is_unreadable(path): if not file_is_readable(path):
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
return None return None
status = -1 status = -1
media = self.VLC.media_new_path(path) if Config.COLON_IN_PATH_FIX:
self.player = media.player_new_from_media() media = self.VLC.media_new_path(path)
self.player = media.player_new_from_media()
else:
self.player = self.VLC.media_player_new(path)
if self.player: if self.player:
self.player.audio_set_volume(self.max_volume) self.player.audio_set_volume(self.max_volume)
status = self.player.play() status = self.player.play()

View File

@ -257,7 +257,7 @@ class MusicMusterSignals(QObject):
emit-a-signal-from-another-class-to-main-class emit-a-signal-from-another-class-to-main-class
""" """
update_row_note_signal = pyqtSignal(int) save_playlist_signal = pyqtSignal()
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
@ -314,7 +314,7 @@ class Window(QMainWindow, Ui_MainWindow):
btn.setEnabled(False) btn.setEnabled(False)
btn.pgb.setVisible(False) btn.pgb.setVisible(False)
if cart.path: if cart.path:
if not helpers.file_is_unreadable(cart.path): if helpers.file_is_readable(cart.path):
colour = Config.COLOUR_CART_READY colour = Config.COLOUR_CART_READY
btn.path = cart.path btn.path = cart.path
btn.player = self.music.VLC.media_player_new(cart.path) btn.player = self.music.VLC.media_player_new(cart.path)
@ -339,7 +339,7 @@ class Window(QMainWindow, Ui_MainWindow):
if not isinstance(btn, CartButton): if not isinstance(btn, CartButton):
return return
if not helpers.file_is_unreadable(btn.path): if helpers.file_is_readable(btn.path):
# Don't allow clicks while we're playing # Don't allow clicks while we're playing
btn.setEnabled(False) btn.setEnabled(False)
if not btn.player: if not btn.player:
@ -377,7 +377,7 @@ class Window(QMainWindow, Ui_MainWindow):
if not path: if not path:
QMessageBox.warning(self, "Error", "Filename required") QMessageBox.warning(self, "Error", "Filename required")
return return
if cart.path and not helpers.file_is_unreadable(cart.path): if cart.path and helpers.file_is_readable(cart.path):
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
cart.duration = tags['duration'] cart.duration = tags['duration']
@ -530,7 +530,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Attempt to close next track playlist # Attempt to close next track playlist
if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab: if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab:
self.next_track.playlist_tab.clear_next() self.next_track.playlist_tab.mark_unnext()
# Record playlist as closed and update remaining playlist tabs # Record playlist as closed and update remaining playlist tabs
with Session() as session: with Session() as session:
@ -726,9 +726,11 @@ class Window(QMainWindow, Ui_MainWindow):
Actions required: Actions required:
- Set flag to say we're not playing a track - Set flag to say we're not playing a track
- Reset current track
- Tell playlist_tab track has finished - Tell playlist_tab track has finished
- Reset PlaylistTrack objects - Reset current playlist_tab
- Reset clocks - Reset clocks
- Reset end time
- Update headers - Update headers
- Enable controls - Enable controls
""" """
@ -737,7 +739,7 @@ class Window(QMainWindow, Ui_MainWindow):
# doesn't see player=None and kick off end-of-track actions # doesn't see player=None and kick off end-of-track actions
self.playing = False self.playing = False
# Tell playlist_tab track has finished # Remove currently playing track colour
if self.current_track.playlist_tab: if self.current_track.playlist_tab:
self.current_track.playlist_tab.play_ended() self.current_track.playlist_tab.play_ended()
@ -866,8 +868,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.hide_played_tracks = True self.hide_played_tracks = True
self.btnHidePlayed.setText("Show played") self.btnHidePlayed.setText("Show played")
# Update displayed playlist # Update all displayed playlists
self.visible_playlist_tab().hide_or_show_played_tracks() with Session() as session:
for i in range(self.tabPlaylist.count()):
self.tabPlaylist.widget(i).hide_played_tracks(
self.hide_played_tracks)
def import_track(self) -> None: def import_track(self) -> None:
"""Import track file""" """Import track file"""
@ -1266,7 +1271,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.current_track.playlist_tab != self.visible_playlist_tab() self.current_track.playlist_tab != self.visible_playlist_tab()
and self.previous_track.plr_id and self.previous_track.plr_id
): ):
self.previous_track.playlist_tab.clear_next() self.previous_track.playlist_tab.reset_next()
# Update headers # Update headers
self.update_headers() self.update_headers()
@ -1512,9 +1517,9 @@ class Window(QMainWindow, Ui_MainWindow):
# May also be called when last tab is closed # May also be called when last tab is closed
pass pass
def this_is_the_next_playlist_row( def this_is_the_next_playlist_row(self, session: scoped_session,
self, session: scoped_session, plr: PlaylistRows, plr: PlaylistRows,
next_track_playlist_tab: PlaylistTab) -> None: next_track_playlist_tab: PlaylistTab) -> None:
""" """
This is notification from a playlist tab that it holds the next This is notification from a playlist tab that it holds the next
playlist row to be played. playlist row to be played.
@ -1546,7 +1551,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Discard now-incorrect next_track PlaylistTrack and tell # Discard now-incorrect next_track PlaylistTrack and tell
# playlist_tab too # playlist_tab too
self.next_track.playlist_tab.clear_next() self.next_track.playlist_tab.reset_next()
self.clear_next() self.clear_next()
# Populate self.next_track # Populate self.next_track
@ -1607,14 +1612,7 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# If track is playing, update track clocks time and colours # If track is playing, update track clocks time and colours
# There is a discrete time between starting playing a track and if self.music.player and self.music.player.is_playing():
# player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since
# starting play.
if self.music.player and self.current_track.start_time and (
self.music.player.is_playing() or
(datetime.now() - self.current_track.start_time)
< timedelta(microseconds=Config.PLAY_SETTLE)):
playtime = self.music.get_playtime() playtime = self.music.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime) time_to_fade = (self.current_track.fade_at - playtime)
time_to_silence = ( time_to_silence = (
@ -1814,10 +1812,8 @@ class DbDialog(QDialog):
self.musicmuster.visible_playlist_tab().insert_header( self.musicmuster.visible_playlist_tab().insert_header(
self.session, note=self.ui.txtNote.text()) self.session, note=self.ui.txtNote.text())
# TODO: this shouldn't be needed as insert_track() saves # Save to database (which will also commit changes)
# playlist self.musicmuster.visible_playlist_tab().save_playlist(self.session)
# # Save to database (which will also commit changes)
# self.musicmuster.visible_playlist_tab().save_playlist(self.session)
# Clear note field and select search text to make it easier for # Clear note field and select search text to make it easier for
# next search # next search
self.ui.txtNote.clear() self.ui.txtNote.clear()

File diff suppressed because it is too large Load Diff