Merge changes from master
This commit is contained in:
parent
fc4129994b
commit
71e76e02d1
@ -199,8 +199,12 @@ class PlaylistTrack:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Calculate time fade_graph should start updating
|
# Calculate time fade_graph should start updating
|
||||||
update_graph_at_ms = max(0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
|
update_graph_at_ms = max(
|
||||||
self.fade_graph_start_updates = now + dt.timedelta(milliseconds=update_graph_at_ms)
|
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
|
||||||
|
)
|
||||||
|
self.fade_graph_start_updates = now + dt.timedelta(
|
||||||
|
milliseconds=update_graph_at_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@ -84,7 +84,7 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
continue
|
continue
|
||||||
rf = TrackFileData(new_file_path=new_file_path)
|
rf = TrackFileData(new_file_path=new_file_path)
|
||||||
rf.tags = get_tags(new_file_path)
|
rf.tags = get_tags(new_file_path)
|
||||||
if not rf.tags['title'] or not rf.tags['artist']:
|
if not rf.tags["title"] or not rf.tags["artist"]:
|
||||||
show_warning(
|
show_warning(
|
||||||
parent=self.main_window,
|
parent=self.main_window,
|
||||||
title="Error",
|
title="Error",
|
||||||
@ -98,11 +98,11 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
|
|
||||||
# Check for same filename
|
# Check for same filename
|
||||||
match_track = self.check_by_basename(
|
match_track = self.check_by_basename(
|
||||||
session, new_file_path, rf.tags['artist'], rf.tags['title']
|
session, new_file_path, rf.tags["artist"], rf.tags["title"]
|
||||||
)
|
)
|
||||||
if not match_track:
|
if not match_track:
|
||||||
match_track = self.check_by_title(
|
match_track = self.check_by_title(
|
||||||
session, new_file_path, rf.tags['artist'], rf.tags['title']
|
session, new_file_path, rf.tags["artist"], rf.tags["title"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not match_track:
|
if not match_track:
|
||||||
@ -113,8 +113,7 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
# We will store new file in the same directory as the
|
# We will store new file in the same directory as the
|
||||||
# existing file but with the new file name
|
# existing file but with the new file name
|
||||||
rf.track_path = os.path.join(
|
rf.track_path = os.path.join(
|
||||||
os.path.dirname(match_track.path),
|
os.path.dirname(match_track.path), new_file_basename
|
||||||
new_file_basename
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# We will remove existing track file
|
# We will remove existing track file
|
||||||
@ -125,27 +124,34 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
if match_basename == new_file_basename:
|
if match_basename == new_file_basename:
|
||||||
path_text = " " + new_file_basename + " (no change)"
|
path_text = " " + new_file_basename + " (no change)"
|
||||||
else:
|
else:
|
||||||
path_text = f" {match_basename} →\n {new_file_basename} (replace)"
|
path_text = (
|
||||||
|
f" {match_basename} →\n {new_file_basename} (replace)"
|
||||||
|
)
|
||||||
filename_item = QTableWidgetItem(path_text)
|
filename_item = QTableWidgetItem(path_text)
|
||||||
|
|
||||||
if match_track.title == rf.tags['title']:
|
if match_track.title == rf.tags["title"]:
|
||||||
title_text = " " + rf.tags['title'] + " (no change)"
|
title_text = " " + rf.tags["title"] + " (no change)"
|
||||||
else:
|
else:
|
||||||
title_text = f" {match_track.title} →\n {rf.tags['title']} (update)"
|
title_text = (
|
||||||
|
f" {match_track.title} →\n {rf.tags['title']} (update)"
|
||||||
|
)
|
||||||
title_item = QTableWidgetItem(title_text)
|
title_item = QTableWidgetItem(title_text)
|
||||||
|
|
||||||
if match_track.artist == rf.tags['artist']:
|
if match_track.artist == rf.tags["artist"]:
|
||||||
artist_text = " " + rf.tags['artist'] + " (no change)"
|
artist_text = " " + rf.tags["artist"] + " (no change)"
|
||||||
else:
|
else:
|
||||||
artist_text = f" {match_track.artist} →\n {rf.tags['artist']} (update)"
|
artist_text = (
|
||||||
|
f" {match_track.artist} →\n {rf.tags['artist']} (update)"
|
||||||
|
)
|
||||||
artist_item = QTableWidgetItem(artist_text)
|
artist_item = QTableWidgetItem(artist_text)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
rf.track_path = os.path.join(Config.REPLACE_FILES_DEFAULT_DESTINATION,
|
rf.track_path = os.path.join(
|
||||||
new_file_basename)
|
Config.REPLACE_FILES_DEFAULT_DESTINATION, new_file_basename
|
||||||
|
)
|
||||||
filename_item = QTableWidgetItem(" " + new_file_basename + " (new)")
|
filename_item = QTableWidgetItem(" " + new_file_basename + " (new)")
|
||||||
title_item = QTableWidgetItem(" " + rf.tags['title'])
|
title_item = QTableWidgetItem(" " + rf.tags["title"])
|
||||||
artist_item = QTableWidgetItem(" " + rf.tags['artist'])
|
artist_item = QTableWidgetItem(" " + rf.tags["artist"])
|
||||||
|
|
||||||
self.replacement_files.append(rf)
|
self.replacement_files.append(rf)
|
||||||
row = self.ui.tableWidget.rowCount()
|
row = self.ui.tableWidget.rowCount()
|
||||||
|
|||||||
66
app/music.py
66
app/music.py
@ -1,18 +1,23 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import datetime as dt
|
||||||
import threading
|
import threading
|
||||||
|
from time import sleep
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
import vlc # type: ignore
|
import vlc # type: ignore
|
||||||
|
|
||||||
from config import Config
|
# PyQt imports
|
||||||
from helpers import file_is_unreadable
|
|
||||||
from typing import Optional
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QRunnable,
|
QRunnable,
|
||||||
QThreadPool,
|
QThreadPool,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from config import Config
|
||||||
|
from helpers import file_is_unreadable
|
||||||
|
from log import log
|
||||||
|
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
@ -57,6 +62,7 @@ class Music:
|
|||||||
self.VLC = vlc.Instance()
|
self.VLC = vlc.Instance()
|
||||||
self.player = None
|
self.player = None
|
||||||
self.max_volume = Config.VOLUME_VLC_DEFAULT
|
self.max_volume = Config.VOLUME_VLC_DEFAULT
|
||||||
|
self.start_dt: Optional[dt.datetime] = None
|
||||||
|
|
||||||
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
|
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
|
||||||
"""
|
"""
|
||||||
@ -83,6 +89,21 @@ class Music:
|
|||||||
pool = QThreadPool.globalInstance()
|
pool = QThreadPool.globalInstance()
|
||||||
fader = FadeTrack(p, fade_seconds=fade_seconds)
|
fader = FadeTrack(p, fade_seconds=fade_seconds)
|
||||||
pool.start(fader)
|
pool.start(fader)
|
||||||
|
self.start_dt = None
|
||||||
|
|
||||||
|
def get_playtime(self) -> int:
|
||||||
|
"""
|
||||||
|
Return number of milliseconds current track has been playing or
|
||||||
|
zero if not playing. The vlc function get_time() only updates 3-4
|
||||||
|
times a second; this function has much better resolution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.start_dt is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
now = dt.datetime.now()
|
||||||
|
elapsed_seconds = (now - self.start_dt).total_seconds()
|
||||||
|
return int(elapsed_seconds * 1000)
|
||||||
|
|
||||||
def get_position(self) -> Optional[float]:
|
def get_position(self) -> Optional[float]:
|
||||||
"""Return current position"""
|
"""Return current position"""
|
||||||
@ -91,6 +112,23 @@ class Music:
|
|||||||
return None
|
return None
|
||||||
return self.player.get_position()
|
return self.player.get_position()
|
||||||
|
|
||||||
|
def is_playing(self) -> bool:
|
||||||
|
"""Return True if playing"""
|
||||||
|
|
||||||
|
# There is a discrete time between starting playing a track and
|
||||||
|
# player.is_playing() returning True, so assume playing if less
|
||||||
|
# than Config.PLAY_SETTLE microseconds have passed since
|
||||||
|
# starting play.
|
||||||
|
return (
|
||||||
|
self.player is not None
|
||||||
|
and self.start_dt is not None
|
||||||
|
and (
|
||||||
|
self.player.is_playing()
|
||||||
|
or (dt.datetime.now() - self.start_dt)
|
||||||
|
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def play(self, path: str, position: Optional[float] = None) -> None:
|
def play(self, path: str, position: Optional[float] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Start playing the track at path.
|
Start playing the track at path.
|
||||||
@ -109,8 +147,10 @@ class Music:
|
|||||||
if self.player:
|
if self.player:
|
||||||
_ = self.player.play()
|
_ = self.player.play()
|
||||||
self.set_volume(self.max_volume)
|
self.set_volume(self.max_volume)
|
||||||
|
|
||||||
if position:
|
if position:
|
||||||
self.player.set_position(position)
|
self.player.set_position(position)
|
||||||
|
self.start_dt = dt.datetime.now()
|
||||||
|
|
||||||
def set_volume(self, volume=None, set_default=True) -> None:
|
def set_volume(self, volume=None, set_default=True) -> None:
|
||||||
"""Set maximum volume used for player"""
|
"""Set maximum volume used for player"""
|
||||||
@ -125,6 +165,17 @@ class Music:
|
|||||||
volume = Config.VOLUME_VLC_DEFAULT
|
volume = Config.VOLUME_VLC_DEFAULT
|
||||||
|
|
||||||
self.player.audio_set_volume(volume)
|
self.player.audio_set_volume(volume)
|
||||||
|
# Ensure volume correct
|
||||||
|
# For as-yet unknown reasons. sometimes the volume gets
|
||||||
|
# reset to zero within 200mS or so of starting play. This
|
||||||
|
# only happened since moving to Debian 12, which uses
|
||||||
|
# Pipewire for sound (which may be irrelevant).
|
||||||
|
for _ in range(3):
|
||||||
|
current_volume = self.player.audio_get_volume()
|
||||||
|
if current_volume < volume:
|
||||||
|
self.player.audio_set_volume(volume)
|
||||||
|
log.debug(f"Reset from {volume=}")
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
def stop(self) -> float:
|
def stop(self) -> float:
|
||||||
"""Immediately stop playing"""
|
"""Immediately stop playing"""
|
||||||
@ -136,6 +187,7 @@ class Music:
|
|||||||
|
|
||||||
p = self.player
|
p = self.player
|
||||||
self.player = None
|
self.player = None
|
||||||
|
self.start_dt = None
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
position = p.get_position()
|
position = p.get_position()
|
||||||
|
|||||||
@ -781,21 +781,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
self.stop_playing(fade=True)
|
self.stop_playing(fade=True)
|
||||||
|
|
||||||
def get_playtime(self) -> int:
|
|
||||||
"""
|
|
||||||
Return number of milliseconds current track has been playing or
|
|
||||||
zero if not playing. The vlc function get_time() only updates 3-4
|
|
||||||
times a second; this function has much better resolution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if track_sequence.now.track_id is None or track_sequence.now.start_time is None:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
now = dt.datetime.now()
|
|
||||||
track_start = track_sequence.now.start_time
|
|
||||||
elapsed_seconds = (now - track_start).total_seconds()
|
|
||||||
return int(elapsed_seconds * 1000)
|
|
||||||
|
|
||||||
def hide_played(self):
|
def hide_played(self):
|
||||||
"""Toggle hide played tracks"""
|
"""Toggle hide played tracks"""
|
||||||
|
|
||||||
@ -1146,7 +1131,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
- Clear next track
|
- Clear next track
|
||||||
- Restore volume if -3dB active
|
- Restore volume if -3dB active
|
||||||
- Play (new) current track.
|
- Play (new) current track.
|
||||||
- Ensure 100% volume
|
|
||||||
- Show closing volume graph
|
- Show closing volume graph
|
||||||
- Notify model
|
- Notify model
|
||||||
- Note that track is now playing
|
- Note that track is now playing
|
||||||
@ -1167,14 +1151,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# If return is pressed during first PLAY_NEXT_GUARD_MS then
|
# If return is pressed during first PLAY_NEXT_GUARD_MS then
|
||||||
# default to NOT playing the next track, else default to
|
# default to NOT playing the next track, else default to
|
||||||
# playing it.
|
# playing it.
|
||||||
default_yes: bool = (
|
default_yes: bool = track_sequence.now.start_time is not None and (
|
||||||
track_sequence.now.start_time is not None
|
|
||||||
and (
|
|
||||||
(dt.datetime.now() - track_sequence.now.start_time).total_seconds()
|
(dt.datetime.now() - track_sequence.now.start_time).total_seconds()
|
||||||
* 1000
|
* 1000
|
||||||
> Config.PLAY_NEXT_GUARD_MS
|
> Config.PLAY_NEXT_GUARD_MS
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if not helpers.ask_yes_no(
|
if not helpers.ask_yes_no(
|
||||||
"Track playing",
|
"Track playing",
|
||||||
"Really play next track now?",
|
"Really play next track now?",
|
||||||
@ -1215,20 +1196,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
return
|
return
|
||||||
self.music.play(track_sequence.now.path, position)
|
self.music.play(track_sequence.now.path, position)
|
||||||
|
|
||||||
# Ensure 100% volume
|
|
||||||
# For as-yet unknown reasons. sometimes the volume gets
|
|
||||||
# reset to zero within 200mS or so of starting play. This
|
|
||||||
# only happened since moving to Debian 12, which uses
|
|
||||||
# Pipewire for sound (which may be irrelevant).
|
|
||||||
for _ in range(3):
|
|
||||||
if self.music.player:
|
|
||||||
volume = self.music.player.audio_get_volume()
|
|
||||||
if volume < Config.VOLUME_VLC_DEFAULT:
|
|
||||||
self.music.set_volume()
|
|
||||||
log.debug(f"Reset from {volume=}")
|
|
||||||
break
|
|
||||||
sleep(0.1)
|
|
||||||
|
|
||||||
# Show closing volume graph
|
# Show closing volume graph
|
||||||
if track_sequence.now.fade_graph:
|
if track_sequence.now.fade_graph:
|
||||||
track_sequence.now.fade_graph.plot()
|
track_sequence.now.fade_graph.plot()
|
||||||
@ -1715,20 +1682,8 @@ 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
|
playtime = self.music.player.get_playtime()
|
||||||
# than Config.PLAY_SETTLE microseconds have passed since
|
|
||||||
# starting play.
|
|
||||||
if (
|
|
||||||
self.music.player
|
|
||||||
and track_sequence.now.start_time
|
|
||||||
and (
|
|
||||||
self.music.player.is_playing()
|
|
||||||
or (dt.datetime.now() - track_sequence.now.start_time)
|
|
||||||
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
playtime = self.get_playtime()
|
|
||||||
time_to_fade = track_sequence.now.fade_at - playtime
|
time_to_fade = track_sequence.now.fade_at - playtime
|
||||||
time_to_silence = track_sequence.now.silence_at - playtime
|
time_to_silence = track_sequence.now.silence_at - playtime
|
||||||
|
|
||||||
|
|||||||
@ -614,7 +614,7 @@ class PlaylistTab(QTableView):
|
|||||||
else:
|
else:
|
||||||
result = self.source_model.get_row_track_path(model_row_number)
|
result = self.source_model.get_row_track_path(model_row_number)
|
||||||
|
|
||||||
log.info(f"get_selected_row_track_path() returned: {result=}")
|
log.debug(f"get_selected_row_track_path() returned: {result=}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_selected_rows(self) -> List[int]:
|
def get_selected_rows(self) -> List[int]:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user