From 8d502afcee627f18458587967d19118c66d21fa2 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Fri, 31 Jan 2025 17:52:28 +0100 Subject: [PATCH 001/109] Added flac to supported formats. --- wobuzz/player/track.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index f80e1d2..0a337d0 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -9,7 +9,8 @@ from tinytag import TinyTag SUPPORTED_FORMATS = [ "mp3", "wav", - "ogg" + "ogg", + "flac", ] From b77df6987ccf4c5da29fdd5b5fbee68faa41b1f9 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Fri, 31 Jan 2025 20:46:15 +0100 Subject: [PATCH 002/109] Added m4a to supported formats. --- wobuzz/player/track.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 0a337d0..7558c50 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -11,6 +11,7 @@ SUPPORTED_FORMATS = [ "wav", "ogg", "flac", + "m4a" ] From ed92c46f9500cc3a6d134c431df9665f138ccf53 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Fri, 31 Jan 2025 20:47:39 +0100 Subject: [PATCH 003/109] Completely removed supported formats check because Pydub is compatible with almost every format. --- wobuzz/player/track.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 7558c50..a75e22b 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -1,20 +1,10 @@ #!/usr/bin/python3 from pydub import AudioSegment -from pydub.effects import normalize from pygame.mixer import Sound from tinytag import TinyTag -SUPPORTED_FORMATS = [ - "mp3", - "wav", - "ogg", - "flac", - "m4a" -] - - class Track: """ Class containing data for a track like file path, raw data... @@ -86,10 +76,9 @@ class Track: self.duration = 0 def load_audio(self): - file_type = self.path.split(".")[-1] + #file_type = self.path.split(".")[-1] - if file_type in SUPPORTED_FORMATS: - self.audio = AudioSegment.from_file(self.path) + self.audio = AudioSegment.from_file(self.path) def remaining(self, position: int): remaining_audio = self.audio[position:] From 060132be367a26514ad60e427073ec8cd03ee5ff Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Fri, 31 Jan 2025 21:25:46 +0100 Subject: [PATCH 004/109] Added icon and desktop entry. --- setup.py | 12 +++++- wobuzz.desktop | 10 +++++ wobuzz/icon.svg | 90 ++++++++++++++++++++++++++++++++++++++++ wobuzz/ui/main_window.py | 4 ++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 wobuzz.desktop create mode 100644 wobuzz/icon.svg diff --git a/setup.py b/setup.py index e86c545..dc84ddb 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,20 @@ #!/usr/bin/python3 import setuptools +import os from pathlib import Path +import shutil + +this_directory = Path(__file__).parent +desktop_entry_path = os.path.expanduser("~/.local/share/applications") +icon_path = os.path.expanduser("~/.local/share/icons/hicolor/scalable/apps") + +os.makedirs(icon_path, exist_ok=True) + +shutil.copy(f"{this_directory}/wobuzz.desktop", f"{desktop_entry_path}/wobuzz.desktop") # install desktop entry +shutil.copy(f"{this_directory}/wobuzz/icon.svg", f"{icon_path}/wobuzz.svg") # install icon # use readme file as long description -this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() setuptools.setup( diff --git a/wobuzz.desktop b/wobuzz.desktop new file mode 100644 index 0000000..6936e52 --- /dev/null +++ b/wobuzz.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=Wobuzz +Comment=A simple audio player +Keywords=audio;music;player;wobuzz;wobbl;qt; +Categories=Multimedia;Media;AudioVideo;Audio;Player;Music;Qt; +Exec=wobuzz %F +Icon=wobuzz +Terminal=false +MimeType=audio/mpeg;audio/x-wav;audio/x-flac;audio/x-aiff;audio/x-m4a;audio/x-ms-wma;audio/x-vorbis+ogg; \ No newline at end of file diff --git a/wobuzz/icon.svg b/wobuzz/icon.svg new file mode 100644 index 0000000..d4f6b27 --- /dev/null +++ b/wobuzz/icon.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index f8aa7ac..553f2e7 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QMainWindow, QMenu from .track_control import TrackControl from .settings import Settings @@ -12,7 +13,10 @@ class MainWindow(QMainWindow): self.app = app + self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg") + self.setWindowTitle("Wobuzz") + self.setWindowIcon(self.icon) self.menu_bar = self.menuBar() From 453e1b75b8e296fc1f7806434a0512e6c3d953f8 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Fri, 31 Jan 2025 22:57:35 +0100 Subject: [PATCH 005/109] Linked the latest wobbl tools commit to the setup.py and requirements.txt and added the icon as package data. --- requirements.txt | 3 ++- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0cad0aa..f4f580b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ PyQt6 pygame tinytag -pydub \ No newline at end of file +pydub +wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools \ No newline at end of file diff --git a/setup.py b/setup.py index dc84ddb..fee12a6 100644 --- a/setup.py +++ b/setup.py @@ -27,13 +27,13 @@ setuptools.setup( author="The Wobbler", author_email="emil@i21k.de", packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]), - package_data={"": ["*.txt"]}, + package_data={"": ["*.txt", "*.svg"]}, install_requires=[ "PyQt6", "tinytag", "pydub", "pygame", - "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@main#egg=wobbl_tools" + "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools" ], entry_points={ "console_scripts": ["wobuzz=wobuzz.command_line:main"], From 1fefc76dd795bb2c453a0a6f053469a7a8c80ec1 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 12:39:48 +0100 Subject: [PATCH 006/109] Removed PyQt dev tools from requirements in the README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 698e5fb..9deb50c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To install Wobuzz, you firstly have to install the dependencies that can't be in This can be done using: ``` bash -sudo apt install pyqt6-dev-tools xcb libxcb-cursor0 ffmpeg +sudo apt install xcb libxcb-cursor0 ffmpeg ``` Now you can just clone the repo and let pip install it. From f0215c034a81543d1c2923a17ab300f6afd17f0f Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 12:53:23 +0100 Subject: [PATCH 007/109] Improved installation instructions. --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9deb50c..9dcfdc9 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,26 @@ This can be done using: sudo apt install xcb libxcb-cursor0 ffmpeg ``` -Now you can just clone the repo and let pip install it. +Now, you can install Wobuzz using just one more command: + +```bash +pip install wobuzz@git+https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git#egg=wobuzz +``` + +### Development installation + +If you want to make changes to the code, +you can clone the repo and install it this time using the `-e` parameter, +which will tell pip to not copy the project to `~/.local/lib/python3.x/site-packages`, +but to create symlinks. \ +Using this method, you can put the project wherever you want +(e.g. your Pycharm projects folder) +and the Python-module will always be in sync with the changes you do. ``` bash git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git cd Wobuzz -pip install . +pip install -e . ``` ## Usage: From 429ec8e68315cd4902fddde24eab4eafe1e328e4 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 13:07:10 +0100 Subject: [PATCH 008/109] Fixed a crash that occurred when trying to start an empty playlist. --- wobuzz/ui/playlist_tabs/tab_bar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py index afcfd5f..5fe22dd 100644 --- a/wobuzz/ui/playlist_tabs/tab_bar.py +++ b/wobuzz/ui/playlist_tabs/tab_bar.py @@ -38,7 +38,8 @@ class PlaylistTabBar(QTabBar): playlist_view = self.tab_widget.widget(index) playlist = playlist_view.playlist - self.app.player.start_playlist(playlist) + if playlist.has_tracks(): # dont crash when playlist is empty + self.app.player.start_playlist(playlist) def contextMenuEvent(self, event: QContextMenuEvent, title=None): # get title by self.tabAt() if the event is called from PyQt, else its executed from the tab title and getting From 8dddeac673b3eae9178c9fd9f3e1c1d740ec2e38 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 13:19:43 +0100 Subject: [PATCH 009/109] Adder Playlist.path property to Playlist so we dont always have to get the path by the title. --- wobuzz/player/playlist.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 1160128..26bad40 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -20,6 +20,10 @@ class Playlist: self.current_track: Track | None = None self.view = None + self.path = os.path.expanduser( + f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" + ) + def clear(self): self.sorting: list[Qt.SortOrder] | None = None self.tracks = [] @@ -119,36 +123,29 @@ class Playlist: for track in self.tracks: wbz_data += f"{track.path}\n" - wbz = open( - os.path.expanduser( - f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" - ), - "w" - ) + wbz = open(self.path, "w") wbz.write(wbz_data) wbz.close() def rename(self, title: str): # remove from unique names so a new playlist can have the old name and delete old playlist. - path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" - path = os.path.expanduser(path) - - if os.path.exists(path): - os.remove(os.path.expanduser(path)) + if os.path.exists(self.path): + os.remove(self.path) old_title = self.title self.title = self.app.utils.unique_name(title, ignore=old_title) + self.path = os.path.expanduser( + f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" + ) + if not old_title == self.title: # remove only when the playlist actually has a different name self.app.utils.unique_names.remove(old_title) def delete(self): - path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" - path = os.path.expanduser(path) - - if os.path.exists(path): - os.remove(os.path.expanduser(path)) + if os.path.exists(self.path): + os.remove(self.path) self.app.utils.unique_names.remove(self.title) self.app.library.playlists.remove(self) From ea23f0e127d9caa3b4d7d58e46e08f64e02cfaf3 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 13:35:19 +0100 Subject: [PATCH 010/109] Improved the icon. --- wobuzz/icon.svg | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/wobuzz/icon.svg b/wobuzz/icon.svg index d4f6b27..98885cb 100644 --- a/wobuzz/icon.svg +++ b/wobuzz/icon.svg @@ -8,7 +8,7 @@ version="1.1" id="svg5" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" - sodipodi:docname="logo.svg" + sodipodi:docname="icon.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -27,8 +27,8 @@ inkscape:document-units="px" showgrid="false" inkscape:zoom="0.75989759" - inkscape:cx="452.0346" - inkscape:cy="337.54549" + inkscape:cx="421.10938" + inkscape:cy="458.61443" inkscape:window-width="1920" inkscape:window-height="1023" inkscape:window-x="0" @@ -46,19 +46,14 @@ d="m 182.77204,41.828827 c 26.1142,-31.9117865 53.72304,-45.9799062 62.67325,-40.7688755 8.53202,4.9675519 4.87612,41.4855685 -14.96576,76.6633855 -5.53567,9.814241 -55.03746,-26.937222 -47.70749,-35.89451 z" id="path6997" sodipodi:nodetypes="ssss" /> - + r="79.375" /> + From 85dfa412d00b3c84da0b228dcd8b521372651c14 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 13:36:57 +0100 Subject: [PATCH 011/109] Added build directory to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4a3d3dc..9119131 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ wobuzz/settings.json Wobuzz.egg-info +build __pycache__ .idea \ No newline at end of file From af4f26737709f5ceb950bc89248afc127451305b Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 13:43:49 +0100 Subject: [PATCH 012/109] Fixed another crash that occurred when double-clicking on the tab-bar but not on a tab. --- wobuzz/ui/playlist_tabs/tab_bar.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py index 5fe22dd..38ffaec 100644 --- a/wobuzz/ui/playlist_tabs/tab_bar.py +++ b/wobuzz/ui/playlist_tabs/tab_bar.py @@ -36,6 +36,10 @@ class PlaylistTabBar(QTabBar): def on_doubleclick(self, index): playlist_view = self.tab_widget.widget(index) + + if playlist_view is None: # dont crash if no playlist was double-clicked + return + playlist = playlist_view.playlist if playlist.has_tracks(): # dont crash when playlist is empty From b2a40d008710903c6035aadce6fa09541b1ea8d2 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 14:32:39 +0100 Subject: [PATCH 013/109] Set version to 0.1 alpha 1. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fee12a6..7650ff1 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description = (this_directory / "README.md").read_text() setuptools.setup( name="Wobuzz", - version="0.0", + version="0.1a1", description="An audio player made by The Wobbler", long_description=long_description, long_description_content_type="text/markdown", From 2b8969d92961cba936f2ecab73bf37b7963e0641 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sat, 1 Feb 2025 15:06:29 +0100 Subject: [PATCH 014/109] Improved installation instructions. --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9dcfdc9..351a6fb 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,20 @@ Currently, it just has really basic features but many more things are planned. ## Installation -To install Wobuzz, you firstly have to install the dependencies that can't be installed using pip. -This can be done using: +### Release installation + +Look at the [Releases](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz/releases), +there you can find the commands that you need for the installation. + +### Unstable git installation + +You firstly have to install the newest dependencies: ``` bash sudo apt install xcb libxcb-cursor0 ffmpeg ``` -Now, you can install Wobuzz using just one more command: +Now, you can install the newest unstable version using just one more command: ```bash pip install wobuzz@git+https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git#egg=wobuzz From 83deb903c1188825b10999ee186d18d9720ba164 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sun, 2 Feb 2025 13:47:32 +0100 Subject: [PATCH 015/109] Removed property that was never used and idk what I wanted to do with it. --- wobuzz/player/track.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index a75e22b..60057c2 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -10,10 +10,9 @@ class Track: Class containing data for a track like file path, raw data... """ - def __init__(self, app, path: str, property_string: str=None, cache: bool=False): + def __init__(self, app, path: str, cache: bool=False): self.app = app self.path = path - self.property_string = property_string self.tags = TinyTag.get(self.path, ignore_errors=False, duration=False) From 563aab6204ae19970fffe2042f51a72c675ce9dc Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sun, 2 Feb 2025 14:56:06 +0100 Subject: [PATCH 016/109] Added some comments. --- wobuzz/player/track.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 60057c2..385eaf3 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -33,6 +33,7 @@ class Track: new_occurrences = {} for item in self.items: + # create dict of item: item.index (actually the id of the item bc. the item can't be used as key) playlist_occurrences = new_occurrences.get(item.playlist, {}) playlist_occurrences[id(item)] = item.index From 95d40dd30c6162dc2e9689d64dab2dde289c11e4 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Sun, 2 Feb 2025 16:08:25 +0100 Subject: [PATCH 017/109] Made a small memory optimisation by making the fonts a class variable and not an instance variable. --- wobuzz/ui/playlist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 0a4c047..2cb74b9 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -10,6 +10,10 @@ from .track import TrackItem class PlaylistView(QTreeWidget): itemDropped = pyqtSignal(QTreeWidget, list) + normal_font = QFont() + bold_font = QFont() + bold_font.setBold(True) + def __init__(self, playlist, parent=None): super().__init__(parent) @@ -18,10 +22,6 @@ class PlaylistView(QTreeWidget): playlist.view = self - self.normal_font = QFont() - self.bold_font = QFont() - self.bold_font.setBold(True) - self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) From 67c3b9e226ada66031aa2be6dca2230669c29d3c Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Mon, 3 Feb 2025 14:06:16 +0100 Subject: [PATCH 018/109] Fixed another crash that occurred because of a missing playlist.has_tracks() check. --- wobuzz/ui/track_control.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 76fb391..b34c773 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -84,7 +84,8 @@ class TrackControl(QToolBar): self.app.player.start_playing() elif self.app.player.current_playlist.title == "None": - self.app.player.start_playlist(self.app.gui.clicked_playlist) + if self.app.gui.clicked_playlist.has_tracks(): + self.app.player.start_playlist(self.app.gui.clicked_playlist) def on_playstate_update(self): if self.app.player.playing: From c55c1222f009220e265884bd5a4ef2f80dc1dec5 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Mon, 3 Feb 2025 14:08:19 +0100 Subject: [PATCH 019/109] Added a dock widget that shows background processes. --- wobuzz/gui.py | 8 +++- wobuzz/player/player.py | 4 ++ wobuzz/ui/library_dock.py | 6 --- wobuzz/ui/main_window.py | 5 ++ wobuzz/ui/process/__init__.py | 1 + wobuzz/ui/process/process.py | 41 ++++++++++++++++ wobuzz/ui/process/process_dock.py | 77 +++++++++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 wobuzz/ui/process/__init__.py create mode 100644 wobuzz/ui/process/process.py create mode 100644 wobuzz/ui/process/process_dock.py diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 5bcf85f..87d014b 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -17,7 +17,7 @@ class GUI: self.settings = self.window.settings self.track_control = self.window.track_control - self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.app.library.main_library_dock) + self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock) self.app.library.main_library_dock.setFeatures( QDockWidget.DockWidgetFeature.DockWidgetMovable | @@ -54,3 +54,9 @@ class GUI: self.track_control.on_track_change(previous_track, track) self.app.player.current_playlist.view.on_track_change(previous_track, track) + def on_background_job_start(self, job: str): + self.window.process_dock.job_started_signal.emit(job) + + def on_background_job_stop(self, job: str): + self.window.process_dock.on_background_job_stop(job) + diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 24261a4..5c47a03 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -160,8 +160,12 @@ class Player: track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1] if not track.cached: + self.app.gui.on_background_job_start("track_caching") + track.cache() + self.app.gui.on_background_job_stop("track_caching") + def cache_next_track(self): # function that creates a thread which will cache the next track caching_thread = threading.Thread(target=self.caching_thread_function) diff --git a/wobuzz/ui/library_dock.py b/wobuzz/ui/library_dock.py index 825d6d3..d4694f5 100644 --- a/wobuzz/ui/library_dock.py +++ b/wobuzz/ui/library_dock.py @@ -11,12 +11,6 @@ class LibraryDock(QDockWidget): self.library = library - self.setAllowedAreas( - Qt.DockWidgetArea.LeftDockWidgetArea | - Qt.DockWidgetArea.RightDockWidgetArea | - Qt.DockWidgetArea.BottomDockWidgetArea - ) - self.setAcceptDrops(True) self.library_widget = Library(library, self) diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 553f2e7..4aaa7be 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -5,6 +5,7 @@ from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QMainWindow, QMenu from .track_control import TrackControl from .settings import Settings +from .process.process_dock import ProcessDock class MainWindow(QMainWindow): @@ -35,5 +36,9 @@ class MainWindow(QMainWindow): self.settings.hide() self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.settings) + self.process_dock = ProcessDock(app) + self.process_dock.hide() + self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.process_dock) + self.settings_action.triggered.connect(self.settings.show) diff --git a/wobuzz/ui/process/__init__.py b/wobuzz/ui/process/__init__.py new file mode 100644 index 0000000..a93a4bf --- /dev/null +++ b/wobuzz/ui/process/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/wobuzz/ui/process/process.py b/wobuzz/ui/process/process.py new file mode 100644 index 0000000..80e5fcf --- /dev/null +++ b/wobuzz/ui/process/process.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 + +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import QSizePolicy, QGroupBox, QLabel, QProgressBar, QVBoxLayout + + +class BackgroundProcess(QGroupBox): + normal_font = QFont() + normal_font.setBold(False) + bold_font = QFont() + bold_font.setBold(True) + + def __init__(self, title: str, parent=None, description: str="", steps: int=0): + super().__init__(title, parent) + + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) + self.setFont(self.bold_font) + + self.layout = QVBoxLayout(self) + + self.description = QLabel(description, self) + self.description.setFont(self.normal_font) + self.layout.addWidget(self.description) + + self.progress_bar = QProgressBar(self) + self.progress_bar.setMaximum(steps) + self.layout.addWidget(self.progress_bar) + + self.setLayout(self.layout) + + def get_progress(self): + return 0 + + def set_range(self, maximum: int, minimum: int=0): + self.progress_bar.setRange(minimum, maximum) + + def update_progress(self): + self.progress_bar.setValue(self.get_progress()) + diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py new file mode 100644 index 0000000..161d895 --- /dev/null +++ b/wobuzz/ui/process/process_dock.py @@ -0,0 +1,77 @@ +#!/usr/bin/python3 + +from PyQt6.QtCore import QTimer, pyqtSignal +from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout + +from .process import BackgroundProcess + +PROGRESS_UPDATE_RATE = 60 +PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE + + +class ProcessDock(QDockWidget): + # we need a signal for self.on_background_job_start() because PyQt6 doesn't allow some operations to be performed + # from a different thread + job_started_signal = pyqtSignal(str) + + def __init__(self, app, parent=None): + super().__init__(parent) + + self.app = app + + self.processes = {} + + self.setWindowTitle("Background Processes") + + self.scroll_area = QScrollArea(self) + self.scroll_area.setWidgetResizable(True) + + self.process_container = QWidget(self.scroll_area) + + self.process_layout = QVBoxLayout(self.process_container) + + # add expanding widget so the distance between processes will be equal + self.process_layout.addWidget(QWidget(self.process_container)) + + self.process_container.setLayout(self.process_layout) + + self.scroll_area.setWidget(self.process_container) + + self.setWidget(self.scroll_area) + + self.progress_update_timer = QTimer() + self.progress_update_timer.timeout.connect(self.update_processes) + self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL) + + self.job_started_signal.connect(self.on_background_job_start) + + # for i in range(8): + # self.add_process(BackgroundProcess(f"Kurwa x{i}", self.process_container, "Boooooooober!!!")) + # + # self.add_process(BackgroundProcess("ja perdole!", self.process_container, "Boooooooober!!!")) + + def add_process(self, name: str, process: BackgroundProcess): + if not name in self.processes: + self.processes[name] = process + self.process_layout.insertWidget(self.process_layout.count() - 1, process) + + def update_processes(self): + for process in self.processes.values(): + process.update_progress() + + def on_background_job_start(self, job): + match job: + case "track_caching": + self.add_process( + job, + BackgroundProcess( + "Loading Track", + self.process_container, + "Loading next track in the background so it starts immediately." + ) + ) + + def on_background_job_stop(self, job): + if job in self.processes: + self.processes.pop(job).deleteLater() + From 6eac6468a0815f1853ca45cae332efb56c804d50 Mon Sep 17 00:00:00 2001 From: EKNr1 Date: Mon, 3 Feb 2025 14:12:38 +0100 Subject: [PATCH 020/109] Decreased the background progress update rate a little. --- wobuzz/ui/process/process_dock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py index 161d895..87d9a06 100644 --- a/wobuzz/ui/process/process_dock.py +++ b/wobuzz/ui/process/process_dock.py @@ -5,7 +5,7 @@ from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout from .process import BackgroundProcess -PROGRESS_UPDATE_RATE = 60 +PROGRESS_UPDATE_RATE = 30 PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE From 67d353dcef0a347a12eb5055a607b038e7ded840 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Feb 2025 17:26:00 +0100 Subject: [PATCH 021/109] Removed unused import. --- wobuzz/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wobuzz/main.py b/wobuzz/main.py index ce67244..cae008d 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -import os import sys from PyQt6.QtWidgets import QApplication from wobbl_tools.data_file import load_dataclass_json From bedca22ca6578c81d47a883ee14817cd8838a21b Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Feb 2025 17:53:35 +0100 Subject: [PATCH 022/109] Removed some mechanic that is going to be reimplemented. --- wobuzz/gui.py | 2 -- wobuzz/library/library.py | 14 ++++++-------- wobuzz/main.py | 2 +- wobuzz/player/player.py | 4 ++-- wobuzz/player/playlist.py | 2 -- wobuzz/player/track.py | 15 ++++++++------- wobuzz/player/track_progress_timer.py | 2 +- wobuzz/settings.py | 3 ++- wobuzz/ui/track_control.py | 8 ++++---- 9 files changed, 24 insertions(+), 28 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 87d014b..bd67756 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -11,8 +11,6 @@ class GUI: self.dropped = [] - self.clicked_playlist = self.app.library.temporary_playlist - self.window = MainWindow(app) self.settings = self.window.settings self.track_control = self.window.track_control diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 482df6f..46b2bf6 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -18,8 +18,7 @@ class Library: self.main_library_dock = LibraryDock(self) self.library_docks = [self.main_library_dock] - self.temporary_playlist = Playlist(self.app, "Temporary Playlist") - self.playlists = [self.temporary_playlist] + self.playlists = [] def load(self): path_playlists = os.path.expanduser(f"{self.app.settings.library_path}/playlists") @@ -37,12 +36,8 @@ class Library: if file_name.endswith(".m3u"): path = f"{path_playlists}/{file_name}" - if file_name == "Temporary_Playlist.wbz.m3u": - playlist = self.temporary_playlist - - else: - playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0]) - self.playlists.append(playlist) + playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0]) + self.playlists.append(playlist) playlist.load_from_m3u(path) @@ -62,6 +57,9 @@ class Library: for playlist in self.playlists: playlist.save() + if self.app.player.current_playlist is not None: + self.app.settings.latest_playlist = self.app.player.current_playlist.path + def new_playlist(self): playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist")) self.playlists.append(playlist) diff --git a/wobuzz/main.py b/wobuzz/main.py index cae008d..d5fd0ca 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -19,8 +19,8 @@ class Wobuzz: self.settings = load_dataclass_json(Settings, self.utils.settings_location, self, True, True) self.settings.set_attribute_change_event(self.on_settings_change) - self.player = Player(self) self.library = Library(self) + self.player = Player(self) self.gui = GUI(self) self.post_init() diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 5c47a03..9517862 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -18,7 +18,7 @@ class Player: self.track_progress = TrackProgress(self.app) self.history = Playlist(self.app, "History") - self.current_playlist = Playlist(self.app, "None") + self.current_playlist = None self.playing = False self.paused = False @@ -134,7 +134,7 @@ class Player: self.music_channel.stop() self.track_progress.stop() - if not self.current_playlist.current_track is None: + if self.current_playlist is not None and self.current_playlist.current_track is not None: self.current_sound_duration = self.current_playlist.current_track.duration self.playing = False diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 26bad40..20da8c1 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -72,8 +72,6 @@ class Playlist: if self.current_track is None and self.has_tracks(): self.current_track = self.tracks[0] - #self.app.player.history.append_track(self.current_track) - def load_from_wbz(self, path): pass diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 385eaf3..30785ee 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -48,13 +48,14 @@ class Track: If this track is the currently playing track, and it gets moved, this corrects the current playlist index. """ - if self.app.player.current_playlist.current_track is self: - for item in self.items: - if ( - item.playlist in self.occurrences and - self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index - ): - self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)]) + if self.app.player.current_playlist is not None: + if self.app.player.current_playlist.current_track is self: + for item in self.items: + if ( + item.playlist in self.occurrences and + self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index + ): + self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)]) def cache(self): self.load_audio() diff --git a/wobuzz/player/track_progress_timer.py b/wobuzz/player/track_progress_timer.py index 2a5a66b..0ee77a6 100644 --- a/wobuzz/player/track_progress_timer.py +++ b/wobuzz/player/track_progress_timer.py @@ -30,5 +30,5 @@ class TrackProgress: def stop(self): self.timer.stop() - if not self.app.player.current_playlist.current_track is None: + if self.app.player.current_playlist is not None and self.app.player.current_playlist.current_track is not None: self.remaining_time = self.app.player.current_playlist.current_track.duration diff --git a/wobuzz/settings.py b/wobuzz/settings.py index 46e7a81..37cfa21 100644 --- a/wobuzz/settings.py +++ b/wobuzz/settings.py @@ -8,5 +8,6 @@ class Settings: window_size: tuple[int, int]=None window_maximized: bool=False library_path: str="~/.wobuzz" - clear_track_cache: bool=True + clear_track_cache: bool=True, + latest_playlist: str=None diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index b34c773..eb95ff3 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -80,12 +80,12 @@ class TrackControl(QToolBar): elif self.app.player.playing: # playing self.app.player.pause() - elif self.app.player.current_playlist.has_tracks(): # stopped but tracks in the current playlist + # stopped but tracks in the current playlist + elif self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.start_playing() - elif self.app.player.current_playlist.title == "None": - if self.app.gui.clicked_playlist.has_tracks(): - self.app.player.start_playlist(self.app.gui.clicked_playlist) + elif self.app.player.current_playlist is None: + pass #self.app.player.start_playlist(self.app.gui.clicked_playlist) def on_playstate_update(self): if self.app.player.playing: From d36326c029dfec194b4f601e3e040edfd77a2e1d Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Feb 2025 17:59:37 +0100 Subject: [PATCH 023/109] Replaced shit that got removed in the last commit with something better. --- wobuzz/ui/track_control.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index eb95ff3..4d5ac48 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -85,7 +85,12 @@ class TrackControl(QToolBar): self.app.player.start_playing() elif self.app.player.current_playlist is None: - pass #self.app.player.start_playlist(self.app.gui.clicked_playlist) + if self.app.settings.latest_playlist is not None: + for playlist in self.app.library.playlists: + if playlist.path == self.app.settings.latest_playlist: + self.app.player.start_playlist(playlist) + + break def on_playstate_update(self): if self.app.player.playing: From efe10e7d50182418e7e3bdff9b69443dbc17788d Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Feb 2025 18:00:42 +0100 Subject: [PATCH 024/109] Added a comment. --- wobuzz/ui/track_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 4d5ac48..753bb6b 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -86,7 +86,7 @@ class TrackControl(QToolBar): elif self.app.player.current_playlist is None: if self.app.settings.latest_playlist is not None: - for playlist in self.app.library.playlists: + for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist if playlist.path == self.app.settings.latest_playlist: self.app.player.start_playlist(playlist) From c164201a55fc81ff0c62be4479eea5326b8b1228 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Feb 2025 18:04:04 +0100 Subject: [PATCH 025/109] Just moved some code around. --- wobuzz/library/library.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 46b2bf6..08796f2 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -21,6 +21,9 @@ class Library: self.playlists = [] def load(self): + self.load_playlists() + + def load_playlists(self): path_playlists = os.path.expanduser(f"{self.app.settings.library_path}/playlists") if not os.path.exists(path_playlists): From cf1b4bacd13f0c9e69a250dc6fcec17aec50ecf6 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Tue, 4 Feb 2025 11:17:35 +0100 Subject: [PATCH 026/109] Added some checks so the player doesnt crash because of things removed in latest commits. --- wobuzz/ui/track_control.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 753bb6b..f96d345 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -46,15 +46,15 @@ class TrackControl(QToolBar): self.next_button.triggered.connect(self.next_track) def previous_track(self): - if self.app.player.current_playlist.has_tracks(): + if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.previous_track() def stop(self): - if self.app.player.current_playlist.has_tracks(): + if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.stop() def next_track(self): - if self.app.player.current_playlist.has_tracks(): + if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.next_track() def on_track_change(self, previous_track, track): From 6134c21ce46e065758ebd48f1f0446f09debc370 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Tue, 4 Feb 2025 13:14:15 +0100 Subject: [PATCH 027/109] Made it working again. --- wobuzz/command_line.py | 30 +++++++++++++++++++++--------- wobuzz/library/library.py | 4 ++++ wobuzz/main.py | 3 +-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/wobuzz/command_line.py b/wobuzz/command_line.py index cf9f842..407f1f5 100644 --- a/wobuzz/command_line.py +++ b/wobuzz/command_line.py @@ -4,6 +4,8 @@ import os import sys import argparse +from wobuzz.player.playlist import Playlist + def main(): description = "A music player made by The Wobbler." @@ -20,24 +22,34 @@ def main(): app = Wobuzz() if arguments.playlist: - app.library.temporary_playlist.clear() - app.library.temporary_playlist.view.clear() - app.library.temporary_playlist.load_from_m3u(arguments.playlist) - app.library.temporary_playlist.view.load_tracks() + playlist = Playlist(app, "Temporary Playlist") + + playlist.load_from_m3u(arguments.playlist) + + app.library.playlists.append(playlist) + + if app.library.temporary_playlist in app.library.playlists: + app.library.playlists.remove(app.library.temporary_playlist) + app.library.temporary_playlist = playlist if arguments.track: - app.library.temporary_playlist.clear() - app.library.temporary_playlist.view.clear() - # make track paths absolute tracks = [] for track in arguments.track: tracks.append(os.path.abspath(track)) - app.library.temporary_playlist.load_from_paths(tracks) - app.library.temporary_playlist.view.load_tracks() + playlist = Playlist(app, "Temporary Playlist") + playlist.load_from_paths(tracks) + + app.library.playlists.append(playlist) + + if app.library.temporary_playlist in app.library.playlists: + app.library.playlists.remove(app.library.temporary_playlist) + app.library.temporary_playlist = playlist + + app.post_init() sys.exit(app.qt_app.exec()) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 08796f2..6a60fe4 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -19,6 +19,7 @@ class Library: self.library_docks = [self.main_library_dock] self.playlists = [] + self.temporary_playlist = None def load(self): self.load_playlists() @@ -44,6 +45,9 @@ class Library: playlist.load_from_m3u(path) + if playlist.title == "Temporary Playlist": + self.temporary_playlist = playlist + self.load_playlist_views() def load_playlist_views(self): diff --git a/wobuzz/main.py b/wobuzz/main.py index d5fd0ca..37b5176 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -23,8 +23,6 @@ class Wobuzz: self.player = Player(self) self.gui = GUI(self) - self.post_init() - def post_init(self): self.gui.track_control.track_progress_slider.post_init() self.library.load() @@ -37,4 +35,5 @@ class Wobuzz: if __name__ == "__main__": wobuzz = Wobuzz() + wobuzz.post_init() sys.exit(wobuzz.qt_app.exec()) From 22ffd211df5200952a7ac2d1c427098fa7d86834 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Tue, 4 Feb 2025 14:43:08 +0100 Subject: [PATCH 028/109] Got it working, but it's not better than before... --- wobuzz/command_line.py | 3 ++- wobuzz/gui.py | 5 ++++- wobuzz/library/library.py | 8 ++------ wobuzz/main.py | 2 ++ wobuzz/player/player.py | 10 ++++++++++ wobuzz/player/playlist.py | 28 +++++++++++++++++++++------- wobuzz/ui/playlist.py | 10 +++++++--- 7 files changed, 48 insertions(+), 18 deletions(-) diff --git a/wobuzz/command_line.py b/wobuzz/command_line.py index 407f1f5..1b9ab75 100644 --- a/wobuzz/command_line.py +++ b/wobuzz/command_line.py @@ -20,6 +20,7 @@ def main(): from .main import Wobuzz app = Wobuzz() + app.post_init() if arguments.playlist: playlist = Playlist(app, "Temporary Playlist") @@ -49,7 +50,7 @@ def main(): app.library.playlists.remove(app.library.temporary_playlist) app.library.temporary_playlist = playlist - app.post_init() + app.library.load_playlist_views() sys.exit(app.qt_app.exec()) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index bd67756..fad376f 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -50,7 +50,10 @@ class GUI: def on_track_change(self, previous_track, track): self.track_control.on_track_change(previous_track, track) - self.app.player.current_playlist.view.on_track_change(previous_track, track) + + for dock_id in self.app.player.current_playlist.views: + view = self.app.player.current_playlist.views[dock_id] + view.on_track_change(previous_track, track) def on_background_job_start(self, job: str): self.window.process_dock.job_started_signal.emit(job) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 6a60fe4..787b27d 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -43,13 +43,9 @@ class Library: playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0]) self.playlists.append(playlist) - playlist.load_from_m3u(path) - if playlist.title == "Temporary Playlist": self.temporary_playlist = playlist - self.load_playlist_views() - def load_playlist_views(self): for library_dock in self.library_docks: playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs @@ -57,7 +53,7 @@ class Library: playlist_tabs.playlists = {} for playlist in self.playlists: - playlist_view = PlaylistView(playlist) + playlist_view = PlaylistView(playlist, library_dock) playlist_tabs.addTab(playlist_view, playlist.title) def on_exit(self, event): @@ -74,6 +70,6 @@ class Library: for library_dock in self.library_docks: playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs - playlist_view = PlaylistView(playlist) + playlist_view = PlaylistView(playlist, library_dock) playlist_tabs.addTab(playlist_view, playlist.title) diff --git a/wobuzz/main.py b/wobuzz/main.py index 37b5176..31735b9 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -36,4 +36,6 @@ class Wobuzz: if __name__ == "__main__": wobuzz = Wobuzz() wobuzz.post_init() + wobuzz.library.load_playlist_views() + sys.exit(wobuzz.qt_app.exec()) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 9517862..8fcf749 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +import time import threading import pygame.mixer import pygame.event @@ -174,6 +175,15 @@ class Player: def start_playlist(self, playlist): self.stop() + if not playlist.loaded: + playlist.load() + + while not playlist.has_tracks() and not playlist.loaded: # wait until first track is loaded + time.sleep(0.1) + + if not playlist.has_tracks(): + return + self.current_sound, self.current_sound_duration = playlist.set_track(0) # first track self.current_playlist = playlist diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 20da8c1..f01be81 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import os +import threading from PyQt6.QtCore import Qt from .track import Track @@ -18,7 +19,8 @@ class Playlist: self.tracks: list[Track] = [] self.current_track_index = 0 self.current_track: Track | None = None - self.view = None + self.views = {} # dict of id(LibraryDock): PlaylistView + self.loaded = False self.path = os.path.expanduser( f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" @@ -41,9 +43,13 @@ class Playlist: i += 1 - # set current track to the first track if there is no currently playing track - if self.current_track is None and self.has_tracks(): - self.current_track = self.tracks[0] + self.save() + + self.tracks = [] + + def load(self): + loading_thread = threading.Thread(target=self.load_from_m3u, args=(self.path,)) + loading_thread.start() def load_from_m3u(self, path): file = open(path, "r") @@ -64,14 +70,18 @@ class Playlist: continue - self.tracks.append(Track(self.app, line, cache=i==0)) # first track is cached + self.append_track(Track(self.app, line, cache=i==0)) # first track is cached i += 1 + print("kolupp") + # set current track to the first track if there is no currently playing track if self.current_track is None and self.has_tracks(): self.current_track = self.tracks[0] + self.loaded = True + def load_from_wbz(self, path): pass @@ -149,10 +159,14 @@ class Playlist: self.app.library.playlists.remove(self) def append_track(self, track): + for dock_id in self.views: + view = self.views[dock_id] + view.append_track(track) + self.tracks.append(track) - if self.view: - self.view.append_track(track) + print(track.tags.title) + def h_last_track(self): # get last track in history (only gets used in player.history) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 2cb74b9..57deee5 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -14,13 +14,15 @@ class PlaylistView(QTreeWidget): bold_font = QFont() bold_font.setBold(True) - def __init__(self, playlist, parent=None): + def __init__(self, playlist, dock, parent=None): super().__init__(parent) self.playlist = playlist + self.library_dock = dock + self.app = playlist.app - playlist.view = self + playlist.views[id(dock)] = self self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) @@ -39,7 +41,7 @@ class PlaylistView(QTreeWidget): self.setHeaderLabels(headers) - self.load_tracks() + #self.load_tracks() self.itemActivated.connect(self.on_track_activation) @@ -141,6 +143,8 @@ class PlaylistView(QTreeWidget): if track: playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist + print(self.app.player.current_playlist.current_track_index) + # mark the current track in this playlist item = self.topLevelItem(self.app.player.current_playlist.current_track_index) item.setIcon(0, self.playing_mark) From 0879575882d0ac89a13b7f0b3a79501d3046d1c8 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Tue, 11 Feb 2025 17:34:04 +0100 Subject: [PATCH 029/109] Made the double click working again. --- wobuzz/command_line.py | 1 + wobuzz/player/playlist.py | 18 +++++++++++++++--- wobuzz/ui/playlist_tabs/tab_bar.py | 3 +-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/wobuzz/command_line.py b/wobuzz/command_line.py index 1b9ab75..c6c631c 100644 --- a/wobuzz/command_line.py +++ b/wobuzz/command_line.py @@ -51,6 +51,7 @@ def main(): app.library.temporary_playlist = playlist app.library.load_playlist_views() + sys.exit(app.qt_app.exec()) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index f01be81..49ac134 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -7,10 +7,15 @@ from .track import Track class Playlist: - def __init__(self, app, title: str): + def __init__(self, app, title: str, load_from=None): self.app = app self.title = title # playlist title + # if the playlist is imported and not already in the library, this variable will contain the playlist path or + # track path from which the playlist will get imported + # if None, playlist should be already in the library and will be loaded from a .wbz.m3u + self.load_from = load_from + # add to unique names so if the playlist is loaded from disk, # no other playlist can be created using the same name self.app.utils.unique_names.append(self.title) @@ -48,9 +53,16 @@ class Playlist: self.tracks = [] def load(self): - loading_thread = threading.Thread(target=self.load_from_m3u, args=(self.path,)) + loading_thread = threading.Thread(target=self.loading_thread) loading_thread.start() + def loading_thread(self): + if self.load_from is None: # if the playlist is in the library + self.load_from_wbz(self.path) + + elif self.load_from is list: + pass + def load_from_m3u(self, path): file = open(path, "r") m3u = file.read() @@ -83,7 +95,7 @@ class Playlist: self.loaded = True def load_from_wbz(self, path): - pass + self.load_from_m3u(path) # placeholder def has_tracks(self): return len(self.tracks) > 0 diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py index 38ffaec..4bee5d8 100644 --- a/wobuzz/ui/playlist_tabs/tab_bar.py +++ b/wobuzz/ui/playlist_tabs/tab_bar.py @@ -42,8 +42,7 @@ class PlaylistTabBar(QTabBar): playlist = playlist_view.playlist - if playlist.has_tracks(): # dont crash when playlist is empty - self.app.player.start_playlist(playlist) + self.app.player.start_playlist(playlist) def contextMenuEvent(self, event: QContextMenuEvent, title=None): # get title by self.tabAt() if the event is called from PyQt, else its executed from the tab title and getting From 0c2c91389d2303b581da838417121099698efae8 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 12 Feb 2025 13:50:12 +0100 Subject: [PATCH 030/109] Playlists now get loaded when they are started and removed debug prints. --- wobuzz/command_line.py | 14 ++------------ wobuzz/library/library.py | 3 ++- wobuzz/player/playlist.py | 19 +++++++------------ wobuzz/player/track.py | 2 +- wobuzz/ui/playlist.py | 4 ---- 5 files changed, 12 insertions(+), 30 deletions(-) diff --git a/wobuzz/command_line.py b/wobuzz/command_line.py index c6c631c..6dd56e7 100644 --- a/wobuzz/command_line.py +++ b/wobuzz/command_line.py @@ -23,9 +23,7 @@ def main(): app.post_init() if arguments.playlist: - playlist = Playlist(app, "Temporary Playlist") - - playlist.load_from_m3u(arguments.playlist) + playlist = Playlist(app, "Temporary Playlist", arguments.playlist) app.library.playlists.append(playlist) @@ -34,15 +32,7 @@ def main(): app.library.temporary_playlist = playlist if arguments.track: - # make track paths absolute - tracks = [] - - for track in arguments.track: - tracks.append(os.path.abspath(track)) - - playlist = Playlist(app, "Temporary Playlist") - - playlist.load_from_paths(tracks) + playlist = Playlist(app, "Temporary Playlist", arguments.track) app.library.playlists.append(playlist) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 787b27d..911fb58 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -58,7 +58,8 @@ class Library: def on_exit(self, event): for playlist in self.playlists: - playlist.save() + if playlist.loaded: # only save loaded playlists, unloaded are empty + playlist.save() if self.app.player.current_playlist is not None: self.app.settings.latest_playlist = self.app.player.current_playlist.path diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 49ac134..644006a 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -2,6 +2,7 @@ import os import threading + from PyQt6.QtCore import Qt from .track import Track @@ -44,14 +45,10 @@ class Playlist: path = paths[i] if os.path.isfile(path): - self.tracks.append(Track(self.app, path, cache=i==0)) # first track is cached + self.append_track(Track(self.app, path, cache=i==0)) # first track is cached i += 1 - self.save() - - self.tracks = [] - def load(self): loading_thread = threading.Thread(target=self.loading_thread) loading_thread.start() @@ -60,8 +57,11 @@ class Playlist: if self.load_from is None: # if the playlist is in the library self.load_from_wbz(self.path) - elif self.load_from is list: - pass + elif isinstance(self.load_from, str): + self.load_from_m3u(self.load_from) + + elif isinstance(self.load_from, list): + self.load_from_paths(self.load_from) def load_from_m3u(self, path): file = open(path, "r") @@ -86,8 +86,6 @@ class Playlist: i += 1 - print("kolupp") - # set current track to the first track if there is no currently playing track if self.current_track is None and self.has_tracks(): self.current_track = self.tracks[0] @@ -177,9 +175,6 @@ class Playlist: self.tracks.append(track) - print(track.tags.title) - - def h_last_track(self): # get last track in history (only gets used in player.history) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 30785ee..5007431 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -14,7 +14,7 @@ class Track: self.app = app self.path = path - self.tags = TinyTag.get(self.path, ignore_errors=False, duration=False) + self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) self.cached = False self.audio = None diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 57deee5..df5d6cd 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -41,8 +41,6 @@ class PlaylistView(QTreeWidget): self.setHeaderLabels(headers) - #self.load_tracks() - self.itemActivated.connect(self.on_track_activation) def on_user_sort(self): @@ -143,8 +141,6 @@ class PlaylistView(QTreeWidget): if track: playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist - print(self.app.player.current_playlist.current_track_index) - # mark the current track in this playlist item = self.topLevelItem(self.app.player.current_playlist.current_track_index) item.setIcon(0, self.playing_mark) From 3ac97755bf52536e4cbd18406c9bfc70bb2e9666 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 12 Feb 2025 14:04:11 +0100 Subject: [PATCH 031/109] Set the cursor when hovering over a Playlist title to a normal cursor and made the Player start with the last playlist as active tab. --- wobuzz/library/library.py | 3 +++ wobuzz/ui/playlist_tabs/tab_title.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 911fb58..eb9c044 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -56,6 +56,9 @@ class Library: playlist_view = PlaylistView(playlist, library_dock) playlist_tabs.addTab(playlist_view, playlist.title) + if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened + playlist_tabs.setCurrentIndex(playlist_tabs.count() - 1) + def on_exit(self, event): for playlist in self.playlists: if playlist.loaded: # only save loaded playlists, unloaded are empty diff --git a/wobuzz/ui/playlist_tabs/tab_title.py b/wobuzz/ui/playlist_tabs/tab_title.py index bcae3e5..a7e2cf5 100644 --- a/wobuzz/ui/playlist_tabs/tab_title.py +++ b/wobuzz/ui/playlist_tabs/tab_title.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from PyQt6.QtCore import Qt -from PyQt6.QtGui import QMouseEvent +from PyQt6.QtGui import QMouseEvent, QCursor from PyQt6.QtWidgets import QLineEdit from .tab_bar import PlaylistTabBar @@ -17,9 +17,10 @@ class TabTitle(QLineEdit): self.playlist_view = playlist_view self.setStyleSheet("QLineEdit {background: transparent;}") - self.setFocusPolicy(Qt.FocusPolicy.TabFocus) + self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor) + self.editingFinished.connect(self.on_edit) def mouseDoubleClickEvent(self, event: QMouseEvent): From e5b7ebe6e8bea42cfcfcd1b7f2765d5307cb4eec Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 12 Feb 2025 14:21:59 +0100 Subject: [PATCH 032/109] Removed border from playlist titles. (slightly noticeable) --- wobuzz/ui/playlist_tabs/tab_title.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/ui/playlist_tabs/tab_title.py b/wobuzz/ui/playlist_tabs/tab_title.py index a7e2cf5..5ae4181 100644 --- a/wobuzz/ui/playlist_tabs/tab_title.py +++ b/wobuzz/ui/playlist_tabs/tab_title.py @@ -16,7 +16,7 @@ class TabTitle(QLineEdit): self.index = index self.playlist_view = playlist_view - self.setStyleSheet("QLineEdit {background: transparent;}") + self.setStyleSheet("QLineEdit {background: transparent; border: none;}") self.setFocusPolicy(Qt.FocusPolicy.TabFocus) self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor) From f377263a0a5a5e5b0f0611682158e378c9b92ead Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 12 Feb 2025 14:47:24 +0100 Subject: [PATCH 033/109] Fixed a bug where playlists weren't saved when they weren't loaded from a .m3u. --- wobuzz/player/playlist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 644006a..53554e1 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -49,6 +49,8 @@ class Playlist: i += 1 + self.loaded = True + def load(self): loading_thread = threading.Thread(target=self.loading_thread) loading_thread.start() From d8f885959bbc6636a958df8382f200145aeaaefc Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 12 Feb 2025 14:48:44 +0100 Subject: [PATCH 034/109] Added some comments. --- wobuzz/player/playlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 53554e1..07b305d 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -59,10 +59,10 @@ class Playlist: if self.load_from is None: # if the playlist is in the library self.load_from_wbz(self.path) - elif isinstance(self.load_from, str): + elif isinstance(self.load_from, str): # if it's imported from a .m3u self.load_from_m3u(self.load_from) - elif isinstance(self.load_from, list): + elif isinstance(self.load_from, list): # if it's created from tracks self.load_from_paths(self.load_from) def load_from_m3u(self, path): From db191cbc448a1e48cb09a86a6f6be5eb94c42f94 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 19 Feb 2025 18:57:23 +0100 Subject: [PATCH 035/109] Made drag n drop activate just when the playlist is fully loaded. --- wobuzz/library/library.py | 4 +++- wobuzz/player/playlist.py | 6 ++++++ wobuzz/ui/playlist.py | 1 - 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index eb9c044..f3f1045 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 import os -from PyQt6.QtWidgets import QTabWidget +from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from ..player.playlist import Playlist from ..ui.library_dock import LibraryDock from ..ui.playlist import PlaylistView @@ -75,5 +75,7 @@ class Library: playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs playlist_view = PlaylistView(playlist, library_dock) + playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop + playlist_tabs.addTab(playlist_view, playlist.title) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 07b305d..110adad 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -4,6 +4,7 @@ import os import threading from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QAbstractItemView from .track import Track @@ -65,6 +66,11 @@ class Playlist: elif isinstance(self.load_from, list): # if it's created from tracks self.load_from_paths(self.load_from) + for dock_id in self.views: # enable drag and drop on every view + view = self.views[dock_id] + + view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + def load_from_m3u(self, path): file = open(path, "r") m3u = file.read() diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index df5d6cd..8703bcd 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -24,7 +24,6 @@ class PlaylistView(QTreeWidget): playlist.views[id(dock)] = self - self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setColumnCount(4) From 730e070dfc5801947bbbb001fe8687d510c81e36 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 19 Feb 2025 19:04:36 +0100 Subject: [PATCH 036/109] Fixed a bug by setting Playlist.loaded to True on creation of new playlists so creating new playlists works again. --- wobuzz/library/library.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index f3f1045..9e9ffd6 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -69,6 +69,8 @@ class Library: def new_playlist(self): playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist")) + playlist.loaded = True + self.playlists.append(playlist) for library_dock in self.library_docks: From 5dc91f6605489b3b7e4464627aeaecd63747baad Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 17:17:26 +0100 Subject: [PATCH 037/109] Implemented that the player stops playing when the deleted playlist is the currently playing. --- wobuzz/player/playlist.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 110adad..4f548ea 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -176,6 +176,10 @@ class Playlist: self.app.utils.unique_names.remove(self.title) self.app.library.playlists.remove(self) + if self.app.player.current_playlist == self: # stop if this is the current playlist + self.app.player.stop() + self.app.player.current_playlist = None + def append_track(self, track): for dock_id in self.views: view = self.views[dock_id] From 65564deb821950a4760f8131c4fd7775342f783f Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 17:44:09 +0100 Subject: [PATCH 038/109] Added option to load playlists on start to the settings. --- wobuzz/command_line.py | 1 - wobuzz/library/library.py | 4 ++++ wobuzz/main.py | 6 ++++-- wobuzz/settings.py | 3 ++- wobuzz/ui/settings/behavior.py | 5 ++++- wobuzz/ui/settings/settings.py | 5 +++++ wobuzz/ui/track_progress_slider.py | 2 +- 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/wobuzz/command_line.py b/wobuzz/command_line.py index 6dd56e7..5b97b3a 100644 --- a/wobuzz/command_line.py +++ b/wobuzz/command_line.py @@ -20,7 +20,6 @@ def main(): from .main import Wobuzz app = Wobuzz() - app.post_init() if arguments.playlist: playlist = Playlist(app, "Temporary Playlist", arguments.playlist) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 9e9ffd6..108ab41 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -59,6 +59,10 @@ class Library: if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened playlist_tabs.setCurrentIndex(playlist_tabs.count() - 1) + if self.app.settings.load_on_start: + for playlist in self.playlists: + playlist.load() + def on_exit(self, event): for playlist in self.playlists: if playlist.loaded: # only save loaded playlists, unloaded are empty diff --git a/wobuzz/main.py b/wobuzz/main.py index 31735b9..6a089bc 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -23,8 +23,10 @@ class Wobuzz: self.player = Player(self) self.gui = GUI(self) - def post_init(self): - self.gui.track_control.track_progress_slider.post_init() + self.late_init() + + def late_init(self): + self.gui.track_control.track_progress_slider.late_init() self.library.load() def on_settings_change(self, key, value): diff --git a/wobuzz/settings.py b/wobuzz/settings.py index 37cfa21..43f1a43 100644 --- a/wobuzz/settings.py +++ b/wobuzz/settings.py @@ -8,6 +8,7 @@ class Settings: window_size: tuple[int, int]=None window_maximized: bool=False library_path: str="~/.wobuzz" - clear_track_cache: bool=True, + clear_track_cache: bool=True latest_playlist: str=None + load_on_start: bool=True diff --git a/wobuzz/ui/settings/behavior.py b/wobuzz/ui/settings/behavior.py index 569714e..12c32c5 100644 --- a/wobuzz/ui/settings/behavior.py +++ b/wobuzz/ui/settings/behavior.py @@ -10,5 +10,8 @@ class BehaviourSettings(QWidget): self.layout = QFormLayout(self) self.setLayout(self.layout) + self.load_on_start = QCheckBox(self) + self.layout.addRow("Load playlists on start", self.load_on_start) + self.clear_track_cache = QCheckBox(self) - self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache) \ No newline at end of file + self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache) diff --git a/wobuzz/ui/settings/settings.py b/wobuzz/ui/settings/settings.py index b2809a1..d79f0fe 100644 --- a/wobuzz/ui/settings/settings.py +++ b/wobuzz/ui/settings/settings.py @@ -44,6 +44,7 @@ class Settings(QDockWidget): def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event self.file_settings.library_path_input.setText(self.app.settings.library_path) self.behavior_settings.clear_track_cache.setChecked(self.app.settings.clear_track_cache) + self.behavior_settings.load_on_start.setChecked(self.app.settings.load_on_start) def update_settings(self, key, value): match key: @@ -53,7 +54,11 @@ class Settings(QDockWidget): case "clear_track_cache": self.behavior_settings.clear_track_cache.setDown(value) + case "load_on_start": + self.behavior_settings.load_on_start.setChecked(value) + def write_settings(self): self.app.settings.library_path = self.file_settings.library_path_input.text() self.app.settings.clear_track_cache = self.behavior_settings.clear_track_cache.isChecked() + self.app.settings.load_on_start = self.behavior_settings.load_on_start.isChecked() diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index bc476d0..f08e40d 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -29,7 +29,7 @@ class TrackProgressSlider(QSlider): self.sliderPressed.connect(self.on_press) self.sliderReleased.connect(self.on_release) - def post_init(self): + def late_init(self): self.track_control = self.app.gui.track_control def mousePressEvent(self, event: QMouseEvent): From a81ea15afdca5529cc5612cf8c7ec6e0ce0ada36 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 17:53:03 +0100 Subject: [PATCH 039/109] Fixed another crash that occurred because of another player.current_playlist.has_tracks()-check when the current playlist was None. --- wobuzz/ui/playlist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 8703bcd..e8d4ce4 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -62,7 +62,8 @@ class PlaylistView(QTreeWidget): i += 1 - if self.app.player.current_playlist.has_tracks(): + # make sure the next track is cached (could be moved by user) + if self.app.player.current_playlist == self.playlist and self.app.player.current_playlist.has_tracks(): self.app.player.cache_next_track() def dropEvent(self, event: QDropEvent): From 6786a3dcd879c7afe77d2a6725a5f2f8f1c9e08f Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 18:06:29 +0100 Subject: [PATCH 040/109] Did a little reformatting and removed that commented out test code. xD --- wobuzz/gui.py | 5 +++-- wobuzz/ui/process/process_dock.py | 5 ----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index fad376f..a2bf717 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -14,6 +14,7 @@ class GUI: self.window = MainWindow(app) self.settings = self.window.settings self.track_control = self.window.track_control + self.process_dock = self.window.process_dock self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock) @@ -56,8 +57,8 @@ class GUI: view.on_track_change(previous_track, track) def on_background_job_start(self, job: str): - self.window.process_dock.job_started_signal.emit(job) + self.process_dock.job_started_signal.emit(job) def on_background_job_stop(self, job: str): - self.window.process_dock.on_background_job_stop(job) + self.process_dock.on_background_job_stop(job) diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py index 87d9a06..4715be9 100644 --- a/wobuzz/ui/process/process_dock.py +++ b/wobuzz/ui/process/process_dock.py @@ -45,11 +45,6 @@ class ProcessDock(QDockWidget): self.job_started_signal.connect(self.on_background_job_start) - # for i in range(8): - # self.add_process(BackgroundProcess(f"Kurwa x{i}", self.process_container, "Boooooooober!!!")) - # - # self.add_process(BackgroundProcess("ja perdole!", self.process_container, "Boooooooober!!!")) - def add_process(self, name: str, process: BackgroundProcess): if not name in self.processes: self.processes[name] = process From 301896e12ca437cdfa5f2acb8e7ff46f10446958 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 18:55:01 +0100 Subject: [PATCH 041/109] Added loading of playlists to the process-dock. --- wobuzz/gui.py | 4 ++-- wobuzz/player/player.py | 7 +++++-- wobuzz/player/playlist.py | 29 +++++++++++++++++++++++++++-- wobuzz/ui/process/process_dock.py | 27 ++++++++++++++------------- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index a2bf717..090dd78 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -56,8 +56,8 @@ class GUI: view = self.app.player.current_playlist.views[dock_id] view.on_track_change(previous_track, track) - def on_background_job_start(self, job: str): - self.process_dock.job_started_signal.emit(job) + def on_background_job_start(self, job_name: str, description: str, steps: int=0, getter: any=None): + self.process_dock.job_started_signal.emit(job_name, description, steps, getter) def on_background_job_stop(self, job: str): self.process_dock.on_background_job_stop(job) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 8fcf749..2a2c972 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -161,11 +161,14 @@ class Player: track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1] if not track.cached: - self.app.gui.on_background_job_start("track_caching") + self.app.gui.on_background_job_start( + "Loading Track", + "Loading next track in the background so it starts immediately." + ) track.cache() - self.app.gui.on_background_job_stop("track_caching") + self.app.gui.on_background_job_stop("Loading Track") def cache_next_track(self): # function that creates a thread which will cache the next track diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 4f548ea..60140ab 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -40,9 +40,20 @@ class Playlist: self.current_track = None def load_from_paths(self, paths): + num_tracks = len(paths) + i = 0 - while i < len(paths): + process_title = f'Loading Playlist "{self.title}"' + + self.app.gui.on_background_job_start( + process_title, + f'Loading the tracks of "{self.title}".', + num_tracks, + lambda: i + ) + + while i < num_tracks: path = paths[i] if os.path.isfile(path): @@ -52,6 +63,8 @@ class Playlist: self.loaded = True + self.app.gui.on_background_job_stop(process_title) + def load(self): loading_thread = threading.Thread(target=self.loading_thread) loading_thread.start() @@ -79,9 +92,19 @@ class Playlist: lines = m3u.split("\n") # m3u entries are separated by newlines lines = lines[:-1] # remove last entry because it is just an empty string - i = 0 num_lines = len(lines) + i = 0 + + process_title = f'Loading Playlist "{self.title}"' + + self.app.gui.on_background_job_start( + process_title, + f'Loading the tracks of "{self.title}".', + num_lines, + lambda: i + ) + while i < num_lines: line = lines[i] @@ -100,6 +123,8 @@ class Playlist: self.loaded = True + self.app.gui.on_background_job_stop(process_title) + def load_from_wbz(self, path): self.load_from_m3u(path) # placeholder diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py index 4715be9..3d7c95b 100644 --- a/wobuzz/ui/process/process_dock.py +++ b/wobuzz/ui/process/process_dock.py @@ -5,14 +5,14 @@ from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout from .process import BackgroundProcess -PROGRESS_UPDATE_RATE = 30 +PROGRESS_UPDATE_RATE = 10 PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE class ProcessDock(QDockWidget): # we need a signal for self.on_background_job_start() because PyQt6 doesn't allow some operations to be performed # from a different thread - job_started_signal = pyqtSignal(str) + job_started_signal = pyqtSignal(str, str, int, object) def __init__(self, app, parent=None): super().__init__(parent) @@ -54,17 +54,18 @@ class ProcessDock(QDockWidget): for process in self.processes.values(): process.update_progress() - def on_background_job_start(self, job): - match job: - case "track_caching": - self.add_process( - job, - BackgroundProcess( - "Loading Track", - self.process_container, - "Loading next track in the background so it starts immediately." - ) - ) + def on_background_job_start(self, job_title: str, description: str, steps: int, getter): + process = BackgroundProcess( + job_title, + self.process_container, + description, + steps + ) + + if getter is not None: + process.get_progress = getter + + self.add_process(job_title, process) def on_background_job_stop(self, job): if job in self.processes: From f2f3937fb228ba4be40ec9549a8cbc7e06b7f4d1 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 19:17:44 +0100 Subject: [PATCH 042/109] Fixed a bug where the process widget of the playlist loading thread wouldn't get deleted if the playlist was too short. The bug occurred because the creation of the widget was done through a PyQt-Signal but the deletion occurred in the same thread as the background process. --- wobuzz/gui.py | 4 ++-- wobuzz/ui/process/process_dock.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 090dd78..4463b8a 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -59,6 +59,6 @@ class GUI: def on_background_job_start(self, job_name: str, description: str, steps: int=0, getter: any=None): self.process_dock.job_started_signal.emit(job_name, description, steps, getter) - def on_background_job_stop(self, job: str): - self.process_dock.on_background_job_stop(job) + def on_background_job_stop(self, job_name: str): + self.process_dock.job_finished_signal.emit(job_name) diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py index 3d7c95b..becd170 100644 --- a/wobuzz/ui/process/process_dock.py +++ b/wobuzz/ui/process/process_dock.py @@ -13,6 +13,7 @@ class ProcessDock(QDockWidget): # we need a signal for self.on_background_job_start() because PyQt6 doesn't allow some operations to be performed # from a different thread job_started_signal = pyqtSignal(str, str, int, object) + job_finished_signal = pyqtSignal(str) def __init__(self, app, parent=None): super().__init__(parent) @@ -44,6 +45,7 @@ class ProcessDock(QDockWidget): self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL) self.job_started_signal.connect(self.on_background_job_start) + self.job_finished_signal.connect(self.on_background_job_stop) def add_process(self, name: str, process: BackgroundProcess): if not name in self.processes: From ccda6b30c87e29e468de7b96d8771cf62762a646 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 20 Feb 2025 19:23:31 +0100 Subject: [PATCH 043/109] Added a "View" submenu to the window's menu and an action to the submenu that opens the background processes. --- wobuzz/ui/main_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 4aaa7be..154c820 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -29,6 +29,11 @@ class MainWindow(QMainWindow): self.settings_action = self.edit_menu.addAction("&Settings") + self.view_menu = QMenu("&View", self.menu_bar) + self.menu_bar.addMenu(self.view_menu) + + self.processes_action = self.view_menu.addAction("Show &Background Processes") + self.track_control = TrackControl(app) self.addToolBar(self.track_control) @@ -41,4 +46,5 @@ class MainWindow(QMainWindow): self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.process_dock) self.settings_action.triggered.connect(self.settings.show) + self.processes_action.triggered.connect(self.process_dock.show) From 39bd7e316722bfbd81ddd6738d8e6e5e92cc6d51 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 17:26:47 +0100 Subject: [PATCH 044/109] Added a "track_info"-toolbar which shows an image found in the current audio file's metadata (usually the front-cover) and information about the currently playing track such as title and artist name. --- wobuzz/gui.py | 5 +++ wobuzz/player/player.py | 10 ++--- wobuzz/player/track.py | 4 ++ wobuzz/ui/main_window.py | 4 ++ wobuzz/ui/track_info.py | 83 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 wobuzz/ui/track_info.py diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 4463b8a..4ab74ae 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -15,6 +15,7 @@ class GUI: self.settings = self.window.settings self.track_control = self.window.track_control self.process_dock = self.window.process_dock + self.track_info = self.window.track_info self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock) @@ -62,3 +63,7 @@ class GUI: def on_background_job_stop(self, job_name: str): self.process_dock.job_finished_signal.emit(job_name) + def on_playstate_update(self): + self.track_control.on_playstate_update() + self.track_info.update_info() + diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 2a2c972..82c3d8e 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -33,7 +33,7 @@ class Player: self.playing = True self.paused = False - self.app.gui.track_control.on_playstate_update() + self.app.gui.on_playstate_update() # cache next track so it immediately starts when the current track finishes self.cache_next_track() @@ -68,7 +68,7 @@ class Player: self.app.gui.on_track_change(self.history.h_last_track(), self.current_playlist.current_track) - self.app.gui.track_control.on_playstate_update() + self.app.gui.on_playstate_update() def play_track_in_playlist(self, track_index): self.stop() @@ -99,7 +99,7 @@ class Player: self.track_progress.pause() self.paused = True - self.app.gui.track_control.on_playstate_update() + self.app.gui.on_playstate_update() def unpause(self): self.music_channel.unpause() @@ -108,7 +108,7 @@ class Player: self.playing = True self.paused = False - self.app.gui.track_control.on_playstate_update() + self.app.gui.on_playstate_update() def next_track(self): if not self.current_playlist.on_last_track(): @@ -141,7 +141,7 @@ class Player: self.playing = False self.paused = False - self.app.gui.track_control.on_playstate_update() + self.app.gui.on_playstate_update() def seek(self, position: int): self.music_channel.stop() diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 5007431..77d1d31 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -67,6 +67,8 @@ class Track: self.duration = len(self.audio) # track duration in milliseconds + self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images + self.cached = True def clear_cache(self): @@ -76,6 +78,8 @@ class Track: self.sound = None self.duration = 0 + self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images + def load_audio(self): #file_type = self.path.split(".")[-1] diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 154c820..a430be0 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -6,6 +6,7 @@ from PyQt6.QtWidgets import QMainWindow, QMenu from .track_control import TrackControl from .settings import Settings from .process.process_dock import ProcessDock +from .track_info import TrackInfo class MainWindow(QMainWindow): @@ -45,6 +46,9 @@ class MainWindow(QMainWindow): self.process_dock.hide() self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.process_dock) + self.track_info = TrackInfo(app) + self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.track_info) + self.settings_action.triggered.connect(self.settings.show) self.processes_action.triggered.connect(self.process_dock.show) diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py new file mode 100644 index 0000000..6782a41 --- /dev/null +++ b/wobuzz/ui/track_info.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 + +from PyQt6.QtGui import QPixmap, QFont +from PyQt6.QtWidgets import QToolBar, QWidget, QLabel, QSizePolicy, QVBoxLayout + + +class TrackInfo(QToolBar): + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + + artist_font = QFont() + title_font.setPointSize(12) + + album_font = QFont() + album_font.setPointSize(8) + + def __init__(self, app, parent=None): + super().__init__(parent) + + self.app = app + + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + self.wobuzz_logo = QPixmap(f"{self.app.utils.wobuzz_location}/icon.svg") + + self.track_cover = QLabel(self) + self.track_cover.setFixedSize(64, 64) + self.track_cover.setScaledContents(True) + self.track_cover.setPixmap(self.wobuzz_logo) + self.addWidget(self.track_cover) + + self.info_container = QWidget(self) + info_container_layout = QVBoxLayout(self.info_container) + self.info_container.setLayout(info_container_layout) + self.addWidget(self.info_container) + + self.title = QLabel("Title", self.info_container) + self.title.setFont(self.title_font) + info_container_layout.addWidget(self.title) + + self.artist = QLabel("Artist", self.info_container) + self.artist.setFont(self.artist_font) + info_container_layout.addWidget(self.artist) + + self.album = QLabel("Album", self.info_container) + self.album.setFont(self.album_font) + info_container_layout.addWidget(self.album) + + def update_info(self): + current_playlist = self.app.player.current_playlist + + if current_playlist is not None: + current_track = current_playlist.current_track + title = current_track.tags.title + artist = current_track.tags.artist + album = current_track.tags.album + cover_data = current_track.tags.images.any.data + + self.title.setText(title) + + if artist is not None and not artist == "": + self.artist.setText(f"By {current_track.tags.artist}") + + else: + self.artist.setText("No Artist") + + if album is not None and not album == "": + self.album.setText(f"In {current_track.tags.album}") + + else: + self.album.setText("No Album") + + if isinstance(cover_data, bytes): + cover_pixmap = QPixmap() + cover_pixmap.loadFromData(cover_data) + + self.track_cover.setPixmap(cover_pixmap) + + else: + self.track_cover.setPixmap(self.wobuzz_logo) + + From a2e572cf6ee8d125a1066390ab5dd8b1799f648b Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 17:36:51 +0100 Subject: [PATCH 045/109] Added the "Background Job Monitor" to the list of features. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 351a6fb..3bbf237 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ Currently, it just has really basic features but many more things are planned. ### Features -| Feature | Description | State | -|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented | -| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented | +| Feature | Description | State | +|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented | +| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented | +| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented | ## Installation From 63847f7b421b9b66ce66fcd8f9e48d73740c99cf Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 17:42:38 +0100 Subject: [PATCH 046/109] Changed version to 2nd Alpha of 0.1. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7650ff1..faaaecc 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description = (this_directory / "README.md").read_text() setuptools.setup( name="Wobuzz", - version="0.1a1", + version="0.1a2", description="An audio player made by The Wobbler", long_description=long_description, long_description_content_type="text/markdown", From 6e7948e5799e4c24067222c89fe89e7cb90ffae4 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 17:59:27 +0100 Subject: [PATCH 047/109] Added the license also to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index faaaecc..790fec4 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ setuptools.setup( url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz", author="The Wobbler", author_email="emil@i21k.de", + license="GNU GPLv3", packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]), package_data={"": ["*.txt", "*.svg"]}, install_requires=[ From 567afb186696b4151bb81f6c196772f7d82cd81f Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 20:20:35 +0100 Subject: [PATCH 048/109] Fixed another crash by adding a check that makes sure that the current playlist is not NoneType before a current_playlist.has_tracks()-call. --- wobuzz/ui/track_progress_slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index f08e40d..91e7bb0 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -57,7 +57,7 @@ class TrackProgressSlider(QSlider): def on_release(self): self.dragged = False - if self.app.player.current_playlist.has_tracks(): + if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.seek(self.value()) def update_progress(self): From 851c2306b49d77a2ad32a8b9e4e514250acd4ec3 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 20:37:24 +0100 Subject: [PATCH 049/109] Fixed another crash that occurred because of an unexpected NoneType in the getting of the playing track's cover. --- wobuzz/ui/track_info.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index 6782a41..dc85b86 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -55,7 +55,6 @@ class TrackInfo(QToolBar): title = current_track.tags.title artist = current_track.tags.artist album = current_track.tags.album - cover_data = current_track.tags.images.any.data self.title.setText(title) @@ -71,6 +70,15 @@ class TrackInfo(QToolBar): else: self.album.setText("No Album") + cover = current_track.tags.images.any + + if cover is None: + self.track_cover.setPixmap(self.wobuzz_logo) + + return + + cover_data = cover.data + if isinstance(cover_data, bytes): cover_pixmap = QPixmap() cover_pixmap.loadFromData(cover_data) From 4c0883f6946d55534b5e753b50e6c37b22a1684b Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 21 Feb 2025 21:08:05 +0100 Subject: [PATCH 050/109] Added some features that would be cool in the future. --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3bbf237..cbec4e6 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,14 @@ Currently, it just has really basic features but many more things are planned. ### Features -| Feature | Description | State | -|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented | -| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented | -| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented | +| Feature | Description | State | +|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented | +| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented | +| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented | +| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | Not Implemented | +| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | Not Implemented | +| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | Not Implemented | ## Installation From a23799b6b197658dd56ec25c56d71f0345273962 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 22 Feb 2025 18:25:17 +0100 Subject: [PATCH 051/109] Rearranged some code. --- wobuzz/ui/playlist.py | 18 ++++-------------- wobuzz/ui/track.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index e8d4ce4..03892b4 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -2,7 +2,7 @@ from PyQt6.QtCore import pyqtSignal from PyQt6.QtGui import QDropEvent, QIcon, QFont -from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame +from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView from .track import TrackItem @@ -10,9 +10,7 @@ from .track import TrackItem class PlaylistView(QTreeWidget): itemDropped = pyqtSignal(QTreeWidget, list) - normal_font = QFont() - bold_font = QFont() - bold_font.setBold(True) + playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) def __init__(self, playlist, dock, parent=None): super().__init__(parent) @@ -28,8 +26,6 @@ class PlaylistView(QTreeWidget): self.setColumnCount(4) - self.playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) - headers = [ "#", "Title", @@ -133,20 +129,14 @@ class PlaylistView(QTreeWidget): # unmark the previous track in all playlists for item in previous_track.items: - item.setIcon(0, QIcon(None)) - item.setFont(1, self.normal_font) - item.setFont(2, self.normal_font) - item.setFont(3, self.normal_font) + item.unmark() if track: playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist # mark the current track in this playlist item = self.topLevelItem(self.app.player.current_playlist.current_track_index) - item.setIcon(0, self.playing_mark) - item.setFont(1, self.bold_font) - item.setFont(2, self.bold_font) - item.setFont(3, self.normal_font) + item.mark() def append_track(self, track): TrackItem(track, self.topLevelItemCount() - 1, self) diff --git a/wobuzz/ui/track.py b/wobuzz/ui/track.py index 38ec961..80fa6ee 100644 --- a/wobuzz/ui/track.py +++ b/wobuzz/ui/track.py @@ -1,20 +1,31 @@ #!/usr/bin/python3 from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont, QIcon, QPalette from PyQt6.QtWidgets import QTreeWidgetItem class TrackItem(QTreeWidgetItem): + normal_font = QFont() + bold_font = QFont() + bold_font.setBold(True) + + playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) + def __init__(self, track, index, parent=None): super().__init__(parent) self.track = track self.index_user_sort = index - self.index = index self.playlist = parent.playlist + palette = parent.palette() + + self.highlight_color = palette.color(QPalette.ColorRole.Highlight) + self.base_color = palette.color(QPalette.ColorRole.Base) + track.items.append(self) track.set_occurrences() @@ -31,3 +42,16 @@ class TrackItem(QTreeWidgetItem): self.setText(3, track.tags.album) self.setText(4, str(self.index_user_sort + 1)) + def mark(self): + self.setIcon(0, self.playing_mark) + self.setFont(1, self.bold_font) + self.setFont(2, self.bold_font) + self.setFont(3, self.normal_font) + + + def unmark(self): + self.setIcon(0, QIcon(None)) + self.setFont(1, self.normal_font) + self.setFont(2, self.normal_font) + self.setFont(3, self.normal_font) + From fa323a0a87e26662488a36b7977524ff88bd6f66 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 22 Feb 2025 18:29:54 +0100 Subject: [PATCH 052/109] Added Pip to the requirements. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbec4e6..b0a19ba 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ there you can find the commands that you need for the installation. You firstly have to install the newest dependencies: ``` bash -sudo apt install xcb libxcb-cursor0 ffmpeg +sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip ``` Now, you can install the newest unstable version using just one more command: From 3424b6ed97667945234066760ffc8439a4aff226 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 22 Feb 2025 18:33:25 +0100 Subject: [PATCH 053/109] Added Git to the requirements. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0a19ba..4c99bde 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ there you can find the commands that you need for the installation. You firstly have to install the newest dependencies: ``` bash -sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip +sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip git ``` Now, you can install the newest unstable version using just one more command: From 2388caa3703d889f550229ce5b3df3e3fd52fe14 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 13:54:01 +0100 Subject: [PATCH 054/109] Fixed another missing NoneType check. --- wobuzz/ui/track_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index dc85b86..ea2bbce 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -50,7 +50,7 @@ class TrackInfo(QToolBar): def update_info(self): current_playlist = self.app.player.current_playlist - if current_playlist is not None: + if current_playlist is not None and current_playlist.current_track is not None: current_track = current_playlist.current_track title = current_track.tags.title artist = current_track.tags.artist From 1b69321c05dd45bf304a3cb7ad7bfb6d07b62284 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 13:57:59 +0100 Subject: [PATCH 055/109] Added some shit that happens in update_info() when there is no playing track. --- wobuzz/ui/track_info.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index ea2bbce..b37b9a3 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -88,4 +88,10 @@ class TrackInfo(QToolBar): else: self.track_cover.setPixmap(self.wobuzz_logo) + else: + self.title.setText("No Playing Track") + self.artist.setText("") + self.album.setText("") + self.track_cover.setPixmap(self.wobuzz_logo) + From 3dd9123332fb402a7379c5d68c621b042cc7d0b2 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 16:38:56 +0100 Subject: [PATCH 056/109] Implemented sorting by track title, artist name etc... (Sorting order is not getting saved.) --- wobuzz/player/playlist.py | 41 ++++++++++++++++++++++-- wobuzz/ui/playlist.py | 67 ++++++++++++++++++++++++++++----------- wobuzz/ui/track.py | 13 +++++++- 3 files changed, 100 insertions(+), 21 deletions(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 60140ab..11f087c 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -22,7 +22,15 @@ class Playlist: # no other playlist can be created using the same name self.app.utils.unique_names.append(self.title) - self.sorting: list[Qt.SortOrder] | None = None # Custom sort order if None + # the number is the index of the header section, + # the bool is the sorting order (True = ascending, False = descending) + self.sorting: list[tuple[int, bool]] = [ + (0, True), + (1, True), + (2, True), + (3, True), + (4, True) + ] self.tracks: list[Track] = [] self.current_track_index = 0 self.current_track: Track | None = None @@ -127,6 +135,30 @@ class Playlist: def load_from_wbz(self, path): self.load_from_m3u(path) # placeholder + + def sync(self, view, user_sort: bool=False): + num_tracks = view.topLevelItemCount() + + i = 0 + + while i < num_tracks: + track_item = view.topLevelItem(i) + track = track_item.track + + track_item.index = i + + if user_sort: + track_item.index_user_sort = i + + self.tracks[i] = track + + track.set_occurrences() + + i += 1 + + # make sure the next track is cached (could be moved by user) + if self.app.player.current_playlist == self and self.has_tracks(): + self.app.player.cache_next_track() def has_tracks(self): return len(self.tracks) > 0 @@ -169,7 +201,12 @@ class Playlist: return self.current_track.sound, self.current_track.duration def save(self): - wbz_data = "" + + first_view = list(self.views.values())[0] + first_view.sortItems(4, Qt.SortOrder.AscendingOrder) + self.sync(first_view) + + wbz_data = "#WOBUZZM3U\n" for track in self.tracks: wbz_data += f"{track.path}\n" diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 03892b4..9be7fb2 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -from PyQt6.QtCore import pyqtSignal -from PyQt6.QtGui import QDropEvent, QIcon, QFont +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent, QIcon from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView from .track import TrackItem @@ -20,6 +20,9 @@ class PlaylistView(QTreeWidget): self.app = playlist.app + self.header = self.header() + self.header.setSectionsClickable(True) + playlist.views[id(dock)] = self self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) @@ -37,30 +40,58 @@ class PlaylistView(QTreeWidget): self.setHeaderLabels(headers) self.itemActivated.connect(self.on_track_activation) + self.header.sectionClicked.connect(self.on_header_click) - def on_user_sort(self): + def on_header_click(self, section_index: int): + if section_index == 0: # this would just invert the current sorting + return + + sorting = self.playlist.sorting + last_sort_section_index, order = sorting[4] + + if last_sort_section_index == section_index: + order = not order # invert order + + self.playlist.sorting[4] = (section_index, order) # set sorting + + # convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder + qorder = Qt.SortOrder.AscendingOrder if order else Qt.SortOrder.DescendingOrder + + self.header.setSortIndicator(section_index, qorder) + + else: + del sorting[0] # remove first sort + sorting.append((section_index, True)) # last sort is this section index, ascending + + self.header.setSortIndicator(section_index, Qt.SortOrder.AscendingOrder) + + self.sort() + + def sort(self): + for index, order in self.playlist.sorting: + # convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder + qorder = Qt.SortOrder.AscendingOrder if order else Qt.SortOrder.DescendingOrder + + self.sortItems(index, qorder) + + self.on_sort() + + def on_sort(self, user_sort: bool=False): num_tracks = self.topLevelItemCount() i = 0 while i < num_tracks: - track_item = self.topLevelItem(i) - track = track_item.track - - track_item.index_user_sort = i - track_item.index = i - - track_item.setText(5, str(i + 1)) - - self.playlist.tracks[i] = track - - track.set_occurrences() + track = self.topLevelItem(i) i += 1 - # make sure the next track is cached (could be moved by user) - if self.app.player.current_playlist == self.playlist and self.app.player.current_playlist.has_tracks(): - self.app.player.cache_next_track() + track.setText(0, str(i)) # 0 = index + + if user_sort: + track.setText(4, str(i)) # 4 = user sort index + + self.playlist.sync(self, user_sort) # sync playlist to this view def dropEvent(self, event: QDropEvent): # receive items that were dropped and create new items from its tracks (new items bc. widgets can only have @@ -88,7 +119,7 @@ class PlaylistView(QTreeWidget): event.accept() - self.on_user_sort() + self.on_sort(True) def dragEnterEvent(self, event): # store dragged items in gui.dropped, so the other playlist can receive it diff --git a/wobuzz/ui/track.py b/wobuzz/ui/track.py index 80fa6ee..e1c0f24 100644 --- a/wobuzz/ui/track.py +++ b/wobuzz/ui/track.py @@ -18,6 +18,7 @@ class TrackItem(QTreeWidgetItem): self.track = track self.index_user_sort = index self.index = index + self.parent = parent self.playlist = parent.playlist @@ -48,10 +49,20 @@ class TrackItem(QTreeWidgetItem): self.setFont(2, self.bold_font) self.setFont(3, self.normal_font) - def unmark(self): self.setIcon(0, QIcon(None)) self.setFont(1, self.normal_font) self.setFont(2, self.normal_font) self.setFont(3, self.normal_font) + def __lt__(self, other): + # make numeric strings get sorted the right way + + column = self.parent.sortColumn() + + if column == 0 or column == 4: + return int(self.text(column)) < int(other.text(column)) + + else: + return super().__lt__(other) + From 7205de8389a380f3bb018c93bc349cf451c2b889 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 16:40:17 +0100 Subject: [PATCH 057/109] Fixed a wrong track index calculation. --- wobuzz/ui/playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 9be7fb2..b31e0df 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -170,5 +170,5 @@ class PlaylistView(QTreeWidget): item.mark() def append_track(self, track): - TrackItem(track, self.topLevelItemCount() - 1, self) + TrackItem(track, self.topLevelItemCount(), self) From 894b3d213ab4f3ec39926a9fa514a1afeb03ca1c Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 16:43:19 +0100 Subject: [PATCH 058/109] Made the sort indicator actually show. --- wobuzz/ui/playlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index b31e0df..16a1e10 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -22,6 +22,7 @@ class PlaylistView(QTreeWidget): self.header = self.header() self.header.setSectionsClickable(True) + self.header.setSortIndicatorShown(True) playlist.views[id(dock)] = self From 78b60dba026e5811d8dc128f8f1dd125e8536d45 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 18:05:28 +0100 Subject: [PATCH 059/109] Implemented saving of the sortorder to the .wbz.m3u --- wobuzz/player/playlist.py | 63 +++++++++++++++++++++++++++++++++++++-- wobuzz/player/track.py | 2 +- wobuzz/ui/playlist.py | 7 ++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 11f087c..d1d56bd 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -134,7 +134,63 @@ class Playlist: self.app.gui.on_background_job_stop(process_title) def load_from_wbz(self, path): - self.load_from_m3u(path) # placeholder + file = open(path, "r") + m3u = file.read() + file.close() + + lines = m3u.split("\n") # m3u entries are separated by newlines + lines = lines[:-1] # remove last entry because it is just an empty string + + num_lines = len(lines) + + i = 0 + + process_title = f'Loading Playlist "{self.title}"' + + self.app.gui.on_background_job_start( + process_title, + f'Loading the tracks of "{self.title}".', + num_lines, + lambda: i + ) + + while i < num_lines: + line = lines[i] + + if line.startswith("#"): # comments and EXTM3U/WOBUZZM3U + if line.startswith("#SORT: "): # sort + sort_line = line[6:] # delete "#SORT: " from the line + + sorting = sort_line.split(", ") # split into the sort column specifier and the sort order + # e.g. ["0", "True"] + + del self.sorting[0] # delete first sort so the length stays at 6 + + # convert these from strings back to int and bool and append them to the sorting + self.sorting.append((int(sorting[0]), sorting[1] == "True")) + + i += 1 + + continue + + elif line.startswith("http"): # filter out urls + i += 1 + + continue + + self.append_track(Track(self.app, line, cache=i == 0)) # first track is cached + + i += 1 + + # set current track to the first track if there is no currently playing track + if self.current_track is None and self.has_tracks(): + self.current_track = self.tracks[0] + + list(self.views.values())[0].sort() # execute sort() on the first view + + self.loaded = True + + self.app.gui.on_background_job_stop(process_title) def sync(self, view, user_sort: bool=False): num_tracks = view.topLevelItemCount() @@ -206,7 +262,10 @@ class Playlist: first_view.sortItems(4, Qt.SortOrder.AscendingOrder) self.sync(first_view) - wbz_data = "#WOBUZZM3U\n" + wbz_data = "#WOBUZZM3U\n" # header + + for sort_column, order in self.sorting: + wbz_data += f"#SORT: {sort_column}, {order}\n" for track in self.tracks: wbz_data += f"{track.path}\n" diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 77d1d31..89bcf7e 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -67,7 +67,7 @@ class Track: self.duration = len(self.audio) # track duration in milliseconds - self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images + self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images self.cached = True diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 16a1e10..f37df00 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -9,6 +9,7 @@ from .track import TrackItem class PlaylistView(QTreeWidget): itemDropped = pyqtSignal(QTreeWidget, list) + sort_signal = pyqtSignal(int, Qt.SortOrder) playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) @@ -42,6 +43,7 @@ class PlaylistView(QTreeWidget): self.itemActivated.connect(self.on_track_activation) self.header.sectionClicked.connect(self.on_header_click) + self.sort_signal.connect(self.sortItems) def on_header_click(self, section_index: int): if section_index == 0: # this would just invert the current sorting @@ -73,7 +75,10 @@ class PlaylistView(QTreeWidget): # convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder qorder = Qt.SortOrder.AscendingOrder if order else Qt.SortOrder.DescendingOrder - self.sortItems(index, qorder) + # somehow, QTreeWidget.sortItems() cant be called from a thread, so we have to use a signal to execute it + # in the main thread + self.sort_signal.emit(index, qorder) + # self.sortItems(index, qorder) self.on_sort() From faecea8ca7425cd9dadfd2ff4097bcd1d2ea5795 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 18:16:16 +0100 Subject: [PATCH 060/109] Added setting of last sort to user sort on user sort. --- wobuzz/ui/playlist.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index f37df00..7484215 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -97,6 +97,14 @@ class PlaylistView(QTreeWidget): if user_sort: track.setText(4, str(i)) # 4 = user sort index + if user_sort: + if not self.playlist.sorting[4][0] == 4: # set last sort to user sort + del self.playlist.sorting[0] + + self.playlist.sorting.append((4, True)) + + self.header.setSortIndicator(4, Qt.SortOrder.AscendingOrder) + self.playlist.sync(self, user_sort) # sync playlist to this view def dropEvent(self, event: QDropEvent): From 4dc1caab6ed43c0d44bb52f904832ad6e0f4ce49 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 23 Feb 2025 19:24:11 +0100 Subject: [PATCH 061/109] Added a link to the WOBUZZM3U file format documentation. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4c99bde..adfc311 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Wobuzz is a simple audio player made by The Wobbler. Currently, it just has really basic features but many more things are planned. +The player has its own playlist file format that is similar to extended m3u. [WOBUZZM3U](https://gulm.i21k.de/index.php?title=WOBUZZM3U) ### Features From 3fd29bcf92aa1ece50ec5357f54472247a4f0b92 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 27 Feb 2025 17:26:33 +0100 Subject: [PATCH 062/109] Made playlists load on click if they weren't. Also set default for setting "load_on_start" to False because with this change, it feels a lot cleaner this way and uses less RAM. --- wobuzz/library/library.py | 4 +++- wobuzz/settings.py | 2 +- wobuzz/ui/playlist_tabs/tab_bar.py | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 108ab41..9bc3c9b 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -56,9 +56,11 @@ class Library: playlist_view = PlaylistView(playlist, library_dock) playlist_tabs.addTab(playlist_view, playlist.title) - if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened + if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened and loaded playlist_tabs.setCurrentIndex(playlist_tabs.count() - 1) + playlist.load() + if self.app.settings.load_on_start: for playlist in self.playlists: playlist.load() diff --git a/wobuzz/settings.py b/wobuzz/settings.py index 43f1a43..0c28532 100644 --- a/wobuzz/settings.py +++ b/wobuzz/settings.py @@ -10,5 +10,5 @@ class Settings: library_path: str="~/.wobuzz" clear_track_cache: bool=True latest_playlist: str=None - load_on_start: bool=True + load_on_start: bool=False diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py index 4bee5d8..8519ffd 100644 --- a/wobuzz/ui/playlist_tabs/tab_bar.py +++ b/wobuzz/ui/playlist_tabs/tab_bar.py @@ -32,6 +32,9 @@ class PlaylistTabBar(QTabBar): playlist_view = self.tab_widget.widget(index) playlist = playlist_view.playlist + if not playlist.loaded: + playlist.load() + self.app.gui.clicked_playlist = playlist def on_doubleclick(self, index): From bae644c304614ec8b7017c8999125a66de9afc40 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 27 Feb 2025 17:58:32 +0100 Subject: [PATCH 063/109] Just corrected a PEP E714. --- wobuzz/player/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 82c3d8e..f32d25a 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -89,7 +89,7 @@ class Player: if ( self.app.settings.clear_track_cache and - not last_track is None and + last_track is not None and not last_track == self.current_playlist.current_track ): last_track.clear_cache() From 66ee7d5af621e857a6ac523028390e3389cca41d Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 27 Feb 2025 18:41:35 +0100 Subject: [PATCH 064/109] Added a check to loading_thread() that makes sure that the playlist isn't already loaded. (And probably fixed some bugs without knowing it.) --- wobuzz/player/playlist.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index d1d56bd..c2ca4c7 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -78,6 +78,9 @@ class Playlist: loading_thread.start() def loading_thread(self): + if self.loaded: + return + if self.load_from is None: # if the playlist is in the library self.load_from_wbz(self.path) @@ -313,4 +316,3 @@ class Playlist: if len(self.tracks) > 1: return self.tracks[-2] - From a9f07f071679a51447f89520a862d6c8aa1740d8 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 28 Feb 2025 17:28:14 +0100 Subject: [PATCH 065/109] Implemented loading of tracks via the window's top menubar. --- wobuzz/command_line.py | 14 ++--------- wobuzz/gui.py | 24 +++++++++++++++--- wobuzz/library/library.py | 28 +++++++++++++++++++-- wobuzz/player/playlist.py | 17 ++++++++++--- wobuzz/ui/main_window.py | 8 +++++- wobuzz/ui/playlist.py | 2 +- wobuzz/ui/playlist_tabs/tab_context_menu.py | 2 -- wobuzz/ui/playlist_tabs/tab_widget.py | 6 ++--- 8 files changed, 73 insertions(+), 28 deletions(-) diff --git a/wobuzz/command_line.py b/wobuzz/command_line.py index 5b97b3a..71925fc 100644 --- a/wobuzz/command_line.py +++ b/wobuzz/command_line.py @@ -24,20 +24,10 @@ def main(): if arguments.playlist: playlist = Playlist(app, "Temporary Playlist", arguments.playlist) - app.library.playlists.append(playlist) - - if app.library.temporary_playlist in app.library.playlists: - app.library.playlists.remove(app.library.temporary_playlist) - app.library.temporary_playlist = playlist + app.library.replace_temporary_playlist(playlist) if arguments.track: - playlist = Playlist(app, "Temporary Playlist", arguments.track) - - app.library.playlists.append(playlist) - - if app.library.temporary_playlist in app.library.playlists: - app.library.playlists.remove(app.library.temporary_playlist) - app.library.temporary_playlist = playlist + app.library.open_tracks(arguments.track) app.library.load_playlist_views() diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 4ab74ae..8712c61 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QDockWidget +from PyQt6.QtWidgets import QDockWidget, QFileDialog from .ui.main_window import MainWindow @@ -11,7 +11,7 @@ class GUI: self.dropped = [] - self.window = MainWindow(app) + self.window = MainWindow(app, self) self.settings = self.window.settings self.track_control = self.window.track_control self.process_dock = self.window.process_dock @@ -27,9 +27,14 @@ class GUI: if self.app.settings.window_maximized: self.window.showMaximized() - elif not self.app.settings.window_size is None: + elif self.app.settings.window_size is not None: self.window.resize(*self.app.settings.window_size) + self.audio_file_selector = QFileDialog(self.window, "Select Audio File") + self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) + self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus)", "Any (*)"]) + self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List) + self.connect() self.window.show() @@ -67,3 +72,16 @@ class GUI: self.track_control.on_playstate_update() self.track_info.update_info() + def select_audio_files(self): + if self.audio_file_selector.exec(): + return self.audio_file_selector.selectedFiles() + + def open_tracks(self): + files = self.select_audio_files() + + if files is not None and not files == []: + self.app.library.open_tracks(files) + + def import_tracks(self): + self.open_tracks() # placeholder + diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 9bc3c9b..549bdce 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -47,12 +47,16 @@ class Library: self.temporary_playlist = playlist def load_playlist_views(self): + # create views for each dock and playlist + for library_dock in self.library_docks: playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs - playlist_tabs.playlists = {} - + # create view for each playlist for playlist in self.playlists: + if id(library_dock) in playlist.views: # view already exists + continue + playlist_view = PlaylistView(playlist, library_dock) playlist_tabs.addTab(playlist_view, playlist.title) @@ -87,3 +91,23 @@ class Library: playlist_tabs.addTab(playlist_view, playlist.title) + def replace_temporary_playlist(self, replace: Playlist): + self.temporary_playlist.delete() + + if self.temporary_playlist in self.playlists: + self.playlists.remove(self.temporary_playlist) + + if not replace in self.playlists: + self.playlists.append(replace) + + self.temporary_playlist = replace + + def open_tracks(self, tracks: list[str]): + playlist = Playlist(self.app, "Temporary Playlist", tracks) + + self.replace_temporary_playlist(playlist) + + self.load_playlist_views() + + playlist.load() + diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index c2ca4c7..f52af29 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -36,6 +36,7 @@ class Playlist: self.current_track: Track | None = None self.views = {} # dict of id(LibraryDock): PlaylistView self.loaded = False + self.loading = False self.path = os.path.expanduser( f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" @@ -78,9 +79,11 @@ class Playlist: loading_thread.start() def loading_thread(self): - if self.loaded: + if self.loaded or self.loading: return + self.loading = True + if self.load_from is None: # if the playlist is in the library self.load_from_wbz(self.path) @@ -90,6 +93,8 @@ class Playlist: elif isinstance(self.load_from, list): # if it's created from tracks self.load_from_paths(self.load_from) + self.loading = False + for dock_id in self.views: # enable drag and drop on every view view = self.views[dock_id] @@ -297,13 +302,16 @@ class Playlist: if os.path.exists(self.path): os.remove(self.path) - self.app.utils.unique_names.remove(self.title) - self.app.library.playlists.remove(self) - if self.app.player.current_playlist == self: # stop if this is the current playlist self.app.player.stop() self.app.player.current_playlist = None + for view in self.views.values(): # close views (and PyQt automatically closes the corresponding tabs) + view.deleteLater() + + self.app.utils.unique_names.remove(self.title) + self.app.library.playlists.remove(self) + def append_track(self, track): for dock_id in self.views: view = self.views[dock_id] @@ -316,3 +324,4 @@ class Playlist: if len(self.tracks) > 1: return self.tracks[-2] + diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index a430be0..53b5be6 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -10,10 +10,11 @@ from .track_info import TrackInfo class MainWindow(QMainWindow): - def __init__(self, app, parent=None): + def __init__(self, app, gui, parent=None): super().__init__(parent) self.app = app + self.gui = gui self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg") @@ -25,6 +26,9 @@ class MainWindow(QMainWindow): self.file_menu = QMenu("&File", self.menu_bar) self.menu_bar.addMenu(self.file_menu) + self.open_track_action = self.file_menu.addAction("&Open Tracks") + self.import_track_action = self.file_menu.addAction("&Import Track") + self.edit_menu = QMenu("&Edit", self.menu_bar) self.menu_bar.addMenu(self.edit_menu) @@ -51,4 +55,6 @@ class MainWindow(QMainWindow): self.settings_action.triggered.connect(self.settings.show) self.processes_action.triggered.connect(self.process_dock.show) + self.open_track_action.triggered.connect(self.gui.open_tracks) + self.import_track_action.triggered.connect(self.gui.import_tracks) diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 7484215..76ff343 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -25,7 +25,7 @@ class PlaylistView(QTreeWidget): self.header.setSectionsClickable(True) self.header.setSortIndicatorShown(True) - playlist.views[id(dock)] = self + playlist.views[id(dock)] = self # let the playlist know that this view exists self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) diff --git a/wobuzz/ui/playlist_tabs/tab_context_menu.py b/wobuzz/ui/playlist_tabs/tab_context_menu.py index 6e285a2..12ec0df 100644 --- a/wobuzz/ui/playlist_tabs/tab_context_menu.py +++ b/wobuzz/ui/playlist_tabs/tab_context_menu.py @@ -36,5 +36,3 @@ class PlaylistContextMenu(QMenu): def delete(self): self.playlist_title.playlist_view.playlist.delete() - self.playlist_title.playlist_view.deleteLater() - diff --git a/wobuzz/ui/playlist_tabs/tab_widget.py b/wobuzz/ui/playlist_tabs/tab_widget.py index 9eb286e..afdb04b 100644 --- a/wobuzz/ui/playlist_tabs/tab_widget.py +++ b/wobuzz/ui/playlist_tabs/tab_widget.py @@ -20,12 +20,12 @@ class PlaylistTabs(QTabWidget): self.setMovable(True) self.setAcceptDrops(True) - def addTab(self, widget, label): - super().addTab(widget, None) + def addTab(self, playlist_view, label): + super().addTab(playlist_view, None) index = self.tab_bar.count() - 1 - title = TabTitle(self.app, label, self.tab_bar, index, widget) + title = TabTitle(self.app, label, self.tab_bar, index, playlist_view) self.tab_bar.setTabButton(index, QTabBar.ButtonPosition.RightSide, title) From 37f1ea3ff874d4e736f3827a9e2fe2e21fd67f79 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 28 Feb 2025 18:02:06 +0100 Subject: [PATCH 066/109] Fixed another missing NoneType check. --- wobuzz/library/library.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 549bdce..5c70153 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -92,10 +92,11 @@ class Library: playlist_tabs.addTab(playlist_view, playlist.title) def replace_temporary_playlist(self, replace: Playlist): - self.temporary_playlist.delete() + if self.temporary_playlist is not None: + self.temporary_playlist.delete() - if self.temporary_playlist in self.playlists: - self.playlists.remove(self.temporary_playlist) + if self.temporary_playlist in self.playlists: + self.playlists.remove(self.temporary_playlist) if not replace in self.playlists: self.playlists.append(replace) From 5c7f4c4ef7adc52143536178ceb7cbe841928539 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 28 Feb 2025 18:02:37 +0100 Subject: [PATCH 067/109] Implemented importing of playlists via the menubar. --- wobuzz/gui.py | 17 ++++++++++++++++- wobuzz/library/library.py | 9 +++++++++ wobuzz/ui/main_window.py | 8 +++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 8712c61..5bf2dee 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -30,11 +30,16 @@ class GUI: elif self.app.settings.window_size is not None: self.window.resize(*self.app.settings.window_size) - self.audio_file_selector = QFileDialog(self.window, "Select Audio File") + self.audio_file_selector = QFileDialog(self.window, "Select Audio Files") self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus)", "Any (*)"]) self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List) + self.playlist_file_selector = QFileDialog(self.window, "Select Playlist") + self.playlist_file_selector.setFileMode(QFileDialog.FileMode.ExistingFile) + self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"]) + self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) + self.connect() self.window.show() @@ -76,6 +81,10 @@ class GUI: if self.audio_file_selector.exec(): return self.audio_file_selector.selectedFiles() + def select_playlist_file(self): + if self.playlist_file_selector.exec(): + return self.playlist_file_selector.selectedFiles()[0] + def open_tracks(self): files = self.select_audio_files() @@ -85,3 +94,9 @@ class GUI: def import_tracks(self): self.open_tracks() # placeholder + def import_playlist(self): + playlist_path = self.select_playlist_file() + + if playlist_path is not None and not playlist_path == "": + self.app.library.import_playlist(playlist_path) + diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 5c70153..5693db0 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -112,3 +112,12 @@ class Library: playlist.load() + def import_playlist(self, playlist_path: str): + playlist = Playlist(self.app, "Temporary Playlist", playlist_path) + + self.replace_temporary_playlist(playlist) + + self.load_playlist_views() + + playlist.load() + diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 53b5be6..06f219e 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -27,7 +27,12 @@ class MainWindow(QMainWindow): self.menu_bar.addMenu(self.file_menu) self.open_track_action = self.file_menu.addAction("&Open Tracks") - self.import_track_action = self.file_menu.addAction("&Import Track") + self.import_track_action = self.file_menu.addAction("&Import Tracks") + + self.playlist_menu = QMenu("&Playlist", self.menu_bar) + self.menu_bar.addMenu(self.playlist_menu) + + self.import_playlist_action = self.playlist_menu.addAction("&Import Playlists") self.edit_menu = QMenu("&Edit", self.menu_bar) self.menu_bar.addMenu(self.edit_menu) @@ -57,4 +62,5 @@ class MainWindow(QMainWindow): self.processes_action.triggered.connect(self.process_dock.show) self.open_track_action.triggered.connect(self.gui.open_tracks) self.import_track_action.triggered.connect(self.gui.import_tracks) + self.import_playlist_action.triggered.connect(self.gui.import_playlist) From 7fdf7a66a9061d496b57a89c46972e14c0fa0607 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 28 Feb 2025 19:28:07 +0100 Subject: [PATCH 068/109] Fixed https://teapot.informationsanarchistik.de/Wobbl/Wobuzz/issues/12 --- wobuzz/player/playlist.py | 3 +++ wobuzz/player/track.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index f52af29..445c9da 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -309,6 +309,9 @@ class Playlist: for view in self.views.values(): # close views (and PyQt automatically closes the corresponding tabs) view.deleteLater() + for track in self.tracks: # remove items that corresponded to the track and this playlist + track.delete_items(self) + self.app.utils.unique_names.remove(self.title) self.app.library.playlists.remove(self) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 89bcf7e..bf74933 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -22,7 +22,7 @@ class Track: self.duration = 0 self.items = [] - self.occurrences = {} # all occurrences in playlists categorized by playlist and track widget + self.occurrences = {} # all occurrences in playlists categorized by playlist and id of the track widget if cache: self.cache() @@ -94,3 +94,14 @@ class Track: # return the remaining part of the track's audio and the duration of the remaining part return sound, len(remaining_audio) + + def delete_items(self, playlist): + """ + Deletes all QTreeWidgetItems that correspond to this track and the given playlist. + """ + + for item in self.items: + if id(item) in self.occurrences[playlist]: + self.items.remove(item) + + self.occurrences.pop(playlist) From 582448a024f9dce2c8874bc3e1abb73ba3415b89 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 28 Feb 2025 19:38:29 +0100 Subject: [PATCH 069/109] Fixed another crash that was similar to the last one. The crash occurred when the temporary playlist got deleted and then loaded again. It occurred because library.temporary_playlist didn't get set to None on deletion. --- wobuzz/player/playlist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 445c9da..0734dcf 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -312,6 +312,9 @@ class Playlist: for track in self.tracks: # remove items that corresponded to the track and this playlist track.delete_items(self) + if self == self.app.library.temporary_playlist: + self.app.library.temporary_playlist = None + self.app.utils.unique_names.remove(self.title) self.app.library.playlists.remove(self) From 105cc5ddf921056bedbbdf52ccf4b2a0c73ce11b Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 28 Feb 2025 19:46:20 +0100 Subject: [PATCH 070/109] Fixed a bug where a playlist gets deleted on load of another playlist. The bug occurred because the temporary playlist gets deleted when a new playlist gets loaded and I forgot to add code that sets the temporary playlist back to None when the temporary playlist gets renamed. --- wobuzz/player/playlist.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 0734dcf..790d86b 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -295,6 +295,10 @@ class Playlist: f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" ) + # make sure the playlist is not referenced anymore as the temporary playlist + if self == self.app.library.temporary_playlist: + self.app.library.temporary_playlist = None + if not old_title == self.title: # remove only when the playlist actually has a different name self.app.utils.unique_names.remove(old_title) @@ -312,6 +316,7 @@ class Playlist: for track in self.tracks: # remove items that corresponded to the track and this playlist track.delete_items(self) + # make sure the playlist is not referenced as the temporary playlist if self == self.app.library.temporary_playlist: self.app.library.temporary_playlist = None From 8d74c1e14cec7cc384de5f0e78948e30389bd87e Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 17:27:03 +0100 Subject: [PATCH 071/109] Optimized the CPU usage a little by creating one QTimer that updates all progress indicators instead of having a different QTimer for each Widget. --- wobuzz/gui.py | 20 +++++++++++++++----- wobuzz/ui/process/process_dock.py | 9 +-------- wobuzz/ui/track_progress_slider.py | 9 +-------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 5bf2dee..51d2036 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -1,10 +1,14 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, QTimer from PyQt6.QtWidgets import QDockWidget, QFileDialog from .ui.main_window import MainWindow +GUI_UPDATE_RATE = 20 +GUI_UPDATE_INTERVAL = 1000 // GUI_UPDATE_RATE + + class GUI: def __init__(self, app): self.app = app @@ -40,15 +44,16 @@ class GUI: self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"]) self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) - self.connect() + self.gui_update_timer = QTimer() + self.gui_update_timer.timeout.connect(self.update_gui) + self.gui_update_timer.start(GUI_UPDATE_INTERVAL) + + self.window.closeEvent = self.on_exit self.window.show() self.settings.update_all() - def connect(self): - self.window.closeEvent = self.on_exit - def on_exit(self, event): self.app.library.on_exit(event) @@ -100,3 +105,8 @@ class GUI: if playlist_path is not None and not playlist_path == "": self.app.library.import_playlist(playlist_path) + def update_gui(self): + self.track_control.track_progress_slider.update_progress() + if self.process_dock.isVisible(): + self.process_dock.update_processes() + diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py index becd170..e106c59 100644 --- a/wobuzz/ui/process/process_dock.py +++ b/wobuzz/ui/process/process_dock.py @@ -1,13 +1,10 @@ #!/usr/bin/python3 -from PyQt6.QtCore import QTimer, pyqtSignal +from PyQt6.QtCore import pyqtSignal from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout from .process import BackgroundProcess -PROGRESS_UPDATE_RATE = 10 -PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE - class ProcessDock(QDockWidget): # we need a signal for self.on_background_job_start() because PyQt6 doesn't allow some operations to be performed @@ -40,10 +37,6 @@ class ProcessDock(QDockWidget): self.setWidget(self.scroll_area) - self.progress_update_timer = QTimer() - self.progress_update_timer.timeout.connect(self.update_processes) - self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL) - self.job_started_signal.connect(self.on_background_job_start) self.job_finished_signal.connect(self.on_background_job_stop) diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index 91e7bb0..cf7a11b 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -1,12 +1,9 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtCore import Qt from PyQt6.QtGui import QMouseEvent from PyQt6.QtWidgets import QSlider, QStyle, QStyleOptionSlider -PROGRESS_UPDATE_RATE = 60 -PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE - class TrackProgressSlider(QSlider): def __init__(self, app, parent=None): @@ -17,10 +14,6 @@ class TrackProgressSlider(QSlider): self.dragged = False - self.progress_update_timer = QTimer() - self.progress_update_timer.timeout.connect(self.update_progress) - self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL) - option = QStyleOptionSlider() style = self.style() From 012447ca478c1e2811c8923644127f29f3d46f5a Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 17:29:31 +0100 Subject: [PATCH 072/109] Added player.stop() call in the close_event, so the player definitely stops, even when a playlist is still loading / saving. --- wobuzz/gui.py | 1 + wobuzz/ui/track_control.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 51d2036..c0ebf62 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -55,6 +55,7 @@ class GUI: self.settings.update_all() def on_exit(self, event): + self.app.player.stop() self.app.library.on_exit(event) self.app.settings.window_size = (self.window.width(), self.window.height()) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index f96d345..2aa1068 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -42,17 +42,13 @@ class TrackControl(QToolBar): def connect(self): self.previous_button.triggered.connect(self.previous_track) self.toggle_play_button.triggered.connect(self.toggle_playing) - self.stop_button.triggered.connect(self.stop) + self.stop_button.triggered.connect(self.app.player.stop) self.next_button.triggered.connect(self.next_track) def previous_track(self): if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.previous_track() - def stop(self): - if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): - self.app.player.stop() - def next_track(self): if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): self.app.player.next_track() From 83744eb3f4e8e714f233fe511be5223b34183fc4 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 18:30:51 +0100 Subject: [PATCH 073/109] Added a performance description to the README. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index adfc311..8a88b4a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ The player has its own playlist file format that is similar to extended m3u. [WO | Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | Not Implemented | | Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | Not Implemented | +### Performance + +Currently, Wobuzz is relatively CPU-friendly in comparison to other audio players, but not RAM-friendly. +In comparison to Audacious, Wobuzz uses half as much CPU, but more than double the RAM. +This is because Audacious loads the audio only partially, while Wobuzz loads the entire track and the following one. +In the future, this may get optimized and CPU-usage could increase due to more features. + ## Installation ### Release installation From 53c6bccfe655bb7f2812fe2fd73c5cb223ab26de Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 20:03:33 +0100 Subject: [PATCH 074/109] Improved settings layout. --- wobuzz/ui/settings/__init__.py | 2 +- wobuzz/ui/settings/category.py | 31 ++++++++++++++++++ wobuzz/ui/settings/settings.py | 51 +++++++++++++++++++++++------- wobuzz/ui/settings/sub_category.py | 31 ++++++++++++++++++ 4 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 wobuzz/ui/settings/category.py create mode 100644 wobuzz/ui/settings/sub_category.py diff --git a/wobuzz/ui/settings/__init__.py b/wobuzz/ui/settings/__init__.py index 1212f64..0ffb38f 100644 --- a/wobuzz/ui/settings/__init__.py +++ b/wobuzz/ui/settings/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/python3 -from .settings import Settings \ No newline at end of file +from .settings import Settings diff --git a/wobuzz/ui/settings/category.py b/wobuzz/ui/settings/category.py new file mode 100644 index 0000000..9932946 --- /dev/null +++ b/wobuzz/ui/settings/category.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +from PyQt6.QtWidgets import QWidget, QScrollArea, QVBoxLayout + + +class Category(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + self.layout = QVBoxLayout(self) + self.setLayout(self.layout) + + self.scroll_area = QScrollArea(self) + self.scroll_area.setWidgetResizable(True) + + self.settings_container = QWidget(self.scroll_area) + self.settings_layout = QVBoxLayout(self.settings_container) + + # spacer widget to create a sort of list where the subcategory-spacing doesn't depend on the window height + spacer_widget = QWidget(self) + + self.settings_layout.addWidget(spacer_widget) + + self.settings_container.setLayout(self.settings_layout) + + self.scroll_area.setWidget(self.settings_container) + + self.layout.addWidget(self.scroll_area) + + def add_sub_category(self, sub_category): + self.settings_layout.insertWidget(self.settings_layout.count() - 1, sub_category) diff --git a/wobuzz/ui/settings/settings.py b/wobuzz/ui/settings/settings.py index d79f0fe..3396d34 100644 --- a/wobuzz/ui/settings/settings.py +++ b/wobuzz/ui/settings/settings.py @@ -1,9 +1,12 @@ #!/usr/bin/python3 from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QWidget, QDockWidget, QTabWidget, QPushButton, QVBoxLayout +from PyQt6.QtWidgets import QWidget, QDockWidget, QTabWidget, QLineEdit, QCheckBox, QPushButton, QVBoxLayout + from .file import FileSettings from .behavior import BehaviourSettings +from .category import Category +from .sub_category import SubCategory class Settings(QDockWidget): @@ -27,10 +30,34 @@ class Settings(QDockWidget): self.tabs = QTabWidget(self.content) self.content_layout.addWidget(self.tabs) - self.file_settings = FileSettings() + self.file_settings = Category() + + self.file_settings.paths = SubCategory("Paths", "Path related settings") + self.file_settings.add_sub_category(self.file_settings.paths) + + self.file_settings.paths.library_path_input = QLineEdit() + self.file_settings.paths.add_setting("Library Path:", self.file_settings.paths.library_path_input) + self.tabs.addTab(self.file_settings, "Files") - self.behavior_settings = BehaviourSettings() + self.behavior_settings = Category() + + self.behavior_settings.playlist = SubCategory("Playlist", "Playlist behavior") + self.behavior_settings.add_sub_category(self.behavior_settings.playlist) + + self.behavior_settings.playlist.load_on_start = QCheckBox() + self.behavior_settings.playlist.add_setting("Load on start:", self.behavior_settings.playlist.load_on_start) + + self.behavior_settings.track = SubCategory("Track", "Track behavior") + self.behavior_settings.add_sub_category(self.behavior_settings.track) + + self.behavior_settings.track.clear_cache = QCheckBox() + self.behavior_settings.track.add_setting( + "Clear cache:", + self.behavior_settings.track.clear_cache, + "Automatically clear the track's cache after it finished. This greatly reduces RAM usage." + ) + self.tabs.addTab(self.behavior_settings, "Behavior") self.save_button = QPushButton("&Save", self.content) @@ -42,23 +69,23 @@ class Settings(QDockWidget): self.save_button.pressed.connect(self.write_settings) def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event - self.file_settings.library_path_input.setText(self.app.settings.library_path) - self.behavior_settings.clear_track_cache.setChecked(self.app.settings.clear_track_cache) - self.behavior_settings.load_on_start.setChecked(self.app.settings.load_on_start) + self.file_settings.paths.library_path_input.setText(self.app.settings.library_path) + self.behavior_settings.track.clear_cache.setChecked(self.app.settings.clear_track_cache) + self.behavior_settings.playlist.load_on_start.setChecked(self.app.settings.load_on_start) def update_settings(self, key, value): match key: case "library_path": - self.file_settings.library_path_input.setText(value) + self.file_settings.paths.library_path_input.setText(value) case "clear_track_cache": - self.behavior_settings.clear_track_cache.setDown(value) + self.behavior_settings.track.clear_cache.setDown(value) case "load_on_start": - self.behavior_settings.load_on_start.setChecked(value) + self.behavior_settings.playlist.load_on_start.setChecked(value) def write_settings(self): - self.app.settings.library_path = self.file_settings.library_path_input.text() - self.app.settings.clear_track_cache = self.behavior_settings.clear_track_cache.isChecked() - self.app.settings.load_on_start = self.behavior_settings.load_on_start.isChecked() + self.app.settings.library_path = self.file_settings.paths.library_path_input.text() + self.app.settings.clear_track_cache = self.behavior_settings.track.clear_cache.isChecked() + self.app.settings.load_on_start = self.behavior_settings.playlist.load_on_start.isChecked() diff --git a/wobuzz/ui/settings/sub_category.py b/wobuzz/ui/settings/sub_category.py new file mode 100644 index 0000000..f9c9920 --- /dev/null +++ b/wobuzz/ui/settings/sub_category.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import QGroupBox, QLabel, QSizePolicy, QFormLayout + + +class SubCategory(QGroupBox): + description_font = QFont() + description_font.setPointSize(8) + + def __init__(self, title: str, description: str, parent=None): + super().__init__(title, parent) + + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) + + self.layout = QFormLayout() + self.setLayout(self.layout) + + self.description = QLabel(description + "\n", self) + self.layout.addRow(self.description) + + def add_setting(self, text: str, setting, description: str=None): + self.layout.addRow(text, setting) + + if description is not None: + description_label = QLabel(" " + description) + description_label.setFont(self.description_font) + self.layout.addRow(description_label) + From df54239d67b1e649633587d152f01c9f955eeb4c Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 20:11:57 +0100 Subject: [PATCH 075/109] Made the title of the sub-categories bold. --- wobuzz/ui/settings/sub_category.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wobuzz/ui/settings/sub_category.py b/wobuzz/ui/settings/sub_category.py index f9c9920..8ad34f5 100644 --- a/wobuzz/ui/settings/sub_category.py +++ b/wobuzz/ui/settings/sub_category.py @@ -15,6 +15,8 @@ class SubCategory(QGroupBox): self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) + self.setStyleSheet("QGroupBox{font-weight: bold;}") + self.layout = QFormLayout() self.setLayout(self.layout) From cdabced2026718dd45d56db23295a481beae3974 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 20:39:09 +0100 Subject: [PATCH 076/109] Added gui-update-rate-setting. --- wobuzz/gui.py | 9 ++++---- wobuzz/settings.py | 1 + wobuzz/ui/settings/settings.py | 41 +++++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index c0ebf62..64dfc17 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -5,10 +5,6 @@ from PyQt6.QtWidgets import QDockWidget, QFileDialog from .ui.main_window import MainWindow -GUI_UPDATE_RATE = 20 -GUI_UPDATE_INTERVAL = 1000 // GUI_UPDATE_RATE - - class GUI: def __init__(self, app): self.app = app @@ -46,7 +42,7 @@ class GUI: self.gui_update_timer = QTimer() self.gui_update_timer.timeout.connect(self.update_gui) - self.gui_update_timer.start(GUI_UPDATE_INTERVAL) + self.gui_update_timer.start(1000 // self.app.settings.gui_update_rate) self.window.closeEvent = self.on_exit @@ -66,6 +62,9 @@ class GUI: def on_settings_change(self, key, value): self.settings.update_settings(key, value) + if key == "gui_update_rate": + self.gui_update_timer.setInterval(1000 // value) + def on_track_change(self, previous_track, track): self.track_control.on_track_change(previous_track, track) diff --git a/wobuzz/settings.py b/wobuzz/settings.py index 0c28532..a2848c4 100644 --- a/wobuzz/settings.py +++ b/wobuzz/settings.py @@ -11,4 +11,5 @@ class Settings: clear_track_cache: bool=True latest_playlist: str=None load_on_start: bool=False + gui_update_rate: int=20 diff --git a/wobuzz/ui/settings/settings.py b/wobuzz/ui/settings/settings.py index 3396d34..b491835 100644 --- a/wobuzz/ui/settings/settings.py +++ b/wobuzz/ui/settings/settings.py @@ -1,10 +1,18 @@ #!/usr/bin/python3 from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QWidget, QDockWidget, QTabWidget, QLineEdit, QCheckBox, QPushButton, QVBoxLayout +from PyQt6.QtWidgets import ( + QWidget, + QDockWidget, + QTabWidget, + QLineEdit, + QCheckBox, + QPushButton, + QSpinBox, + QVBoxLayout, + QSizePolicy +) -from .file import FileSettings -from .behavior import BehaviourSettings from .category import Category from .sub_category import SubCategory @@ -60,6 +68,28 @@ class Settings(QDockWidget): self.tabs.addTab(self.behavior_settings, "Behavior") + self.performance_settings = Category() + + # self.performance_settings.memory = SubCategory("Memory", "Memory related settings") + # self.performance_settings.add_sub_category(self.performance_settings.memory) + + self.performance_settings.cpu = SubCategory("CPU", "CPU related settings") + self.performance_settings.add_sub_category(self.performance_settings.cpu) + + self.performance_settings.cpu.gui_update_rate = QSpinBox() + self.performance_settings.cpu.gui_update_rate.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.performance_settings.cpu.gui_update_rate.setRange(1, 60) + self.performance_settings.cpu.gui_update_rate.setSuffix(" FPS") + self.performance_settings.cpu.gui_update_rate.setSingleStep(5) + self.performance_settings.cpu.add_setting( + "GUI update rate:", + self.performance_settings.cpu.gui_update_rate, + "The rate at which gui-elements like the track-progress-slider get updated. Values above 20 " + "don't really make sense on most monitors. Decreasing this value will reduce the CPU usage." + ) + + self.tabs.addTab(self.performance_settings, "Performance") + self.save_button = QPushButton("&Save", self.content) self.content_layout.addWidget(self.save_button) @@ -72,6 +102,7 @@ class Settings(QDockWidget): self.file_settings.paths.library_path_input.setText(self.app.settings.library_path) self.behavior_settings.track.clear_cache.setChecked(self.app.settings.clear_track_cache) self.behavior_settings.playlist.load_on_start.setChecked(self.app.settings.load_on_start) + self.performance_settings.cpu.gui_update_rate.setValue(self.app.settings.gui_update_rate) def update_settings(self, key, value): match key: @@ -84,8 +115,12 @@ class Settings(QDockWidget): case "load_on_start": self.behavior_settings.playlist.load_on_start.setChecked(value) + case "gui_update_rate": + self.performance_settings.cpu.gui_update_rate.setValue(value) + def write_settings(self): self.app.settings.library_path = self.file_settings.paths.library_path_input.text() self.app.settings.clear_track_cache = self.behavior_settings.track.clear_cache.isChecked() self.app.settings.load_on_start = self.behavior_settings.playlist.load_on_start.isChecked() + self.app.settings.gui_update_rate = self.performance_settings.cpu.gui_update_rate.value() From a4fa2c7f75dad15ac27067573cc99269b84f5f95 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 1 Mar 2025 22:42:31 +0100 Subject: [PATCH 077/109] Moved track and playlist opening related gui functions to the popups class. --- wobuzz/gui.py | 40 +++++---------------------------- wobuzz/ui/main_window.py | 4 ---- wobuzz/ui/popups.py | 48 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 wobuzz/ui/popups.py diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 64dfc17..dcf53e6 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -2,13 +2,17 @@ from PyQt6.QtCore import Qt, QTimer from PyQt6.QtWidgets import QDockWidget, QFileDialog + from .ui.main_window import MainWindow +from .ui.popups import Popups class GUI: def __init__(self, app): self.app = app + + self.dropped = [] self.window = MainWindow(app, self) @@ -17,6 +21,8 @@ class GUI: self.process_dock = self.window.process_dock self.track_info = self.window.track_info + self.popups = Popups(app, self) + self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock) self.app.library.main_library_dock.setFeatures( @@ -30,16 +36,6 @@ class GUI: elif self.app.settings.window_size is not None: self.window.resize(*self.app.settings.window_size) - self.audio_file_selector = QFileDialog(self.window, "Select Audio Files") - self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) - self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus)", "Any (*)"]) - self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List) - - self.playlist_file_selector = QFileDialog(self.window, "Select Playlist") - self.playlist_file_selector.setFileMode(QFileDialog.FileMode.ExistingFile) - self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"]) - self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) - self.gui_update_timer = QTimer() self.gui_update_timer.timeout.connect(self.update_gui) self.gui_update_timer.start(1000 // self.app.settings.gui_update_rate) @@ -82,31 +78,7 @@ class GUI: self.track_control.on_playstate_update() self.track_info.update_info() - def select_audio_files(self): - if self.audio_file_selector.exec(): - return self.audio_file_selector.selectedFiles() - - def select_playlist_file(self): - if self.playlist_file_selector.exec(): - return self.playlist_file_selector.selectedFiles()[0] - - def open_tracks(self): - files = self.select_audio_files() - - if files is not None and not files == []: - self.app.library.open_tracks(files) - - def import_tracks(self): - self.open_tracks() # placeholder - - def import_playlist(self): - playlist_path = self.select_playlist_file() - - if playlist_path is not None and not playlist_path == "": - self.app.library.import_playlist(playlist_path) - def update_gui(self): self.track_control.track_progress_slider.update_progress() if self.process_dock.isVisible(): self.process_dock.update_processes() - diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 06f219e..738cb70 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -60,7 +60,3 @@ class MainWindow(QMainWindow): self.settings_action.triggered.connect(self.settings.show) self.processes_action.triggered.connect(self.process_dock.show) - self.open_track_action.triggered.connect(self.gui.open_tracks) - self.import_track_action.triggered.connect(self.gui.import_tracks) - self.import_playlist_action.triggered.connect(self.gui.import_playlist) - diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py new file mode 100644 index 0000000..a30d539 --- /dev/null +++ b/wobuzz/ui/popups.py @@ -0,0 +1,48 @@ +#!/usr/bin/python3 + +from PyQt6.QtWidgets import QFileDialog + + +class Popups: + def __init__(self, app, gui): + self.app = app + self.gui = gui + + self.window = gui.window + + self.audio_file_selector = QFileDialog(self.window, "Select Audio Files") + self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) + self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus)", "Any (*)"]) + self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List) + + self.playlist_file_selector = QFileDialog(self.window, "Select Playlist") + self.playlist_file_selector.setFileMode(QFileDialog.FileMode.ExistingFile) + self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"]) + self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) + + self.window.open_track_action.triggered.connect(self.open_tracks) + self.window.import_track_action.triggered.connect(self.import_tracks) + self.window.import_playlist_action.triggered.connect(self.import_playlist) + + def select_audio_files(self): + if self.audio_file_selector.exec(): + return self.audio_file_selector.selectedFiles() + + def select_playlist_file(self): + if self.playlist_file_selector.exec(): + return self.playlist_file_selector.selectedFiles()[0] + + def open_tracks(self): + files = self.select_audio_files() + + if files is not None and not files == []: + self.app.library.open_tracks(files) + + def import_tracks(self): + self.open_tracks() # placeholder + + def import_playlist(self): + playlist_path = self.select_playlist_file() + + if playlist_path is not None and not playlist_path == "": + self.app.library.import_playlist(playlist_path) \ No newline at end of file From 98cce44dc215077bbd1b8fc919478c19d86c7b93 Mon Sep 17 00:00:00 2001 From: Wobbl Date: Sat, 1 Mar 2025 22:03:42 +0000 Subject: [PATCH 078/109] Added information on which repository is the mirror. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8a88b4a..b1415c5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Wobuzz is a simple audio player made by The Wobbler. Currently, it just has really basic features but many more things are planned. The player has its own playlist file format that is similar to extended m3u. [WOBUZZM3U](https://gulm.i21k.de/index.php?title=WOBUZZM3U) +Please note that [the repository on teapot.informationsanarchistik.de](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz) is the original repository and the [repository on Codeberg](https://codeberg.org/Wobbl/Wobuzz) is a mirror, +normal users only have read rights on the original repository and issues are not synced. + ### Features | Feature | Description | State | From 6b808add85a4d9d50d01e61f6888bdc628aefa26 Mon Sep 17 00:00:00 2001 From: Wobbl Date: Sat, 1 Mar 2025 22:14:03 +0000 Subject: [PATCH 079/109] Improved information about the repository syncing. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1415c5..73622af 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ Currently, it just has really basic features but many more things are planned. The player has its own playlist file format that is similar to extended m3u. [WOBUZZM3U](https://gulm.i21k.de/index.php?title=WOBUZZM3U) Please note that [the repository on teapot.informationsanarchistik.de](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz) is the original repository and the [repository on Codeberg](https://codeberg.org/Wobbl/Wobuzz) is a mirror, -normal users only have read rights on the original repository and issues are not synced. +normal users only have read rights on the original repository because registration is disabled on the server. +Issues and pull-requests are not synced. ### Features From 67f27c8a15887abc8334756cbfe8ccebf0807d20 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 2 Mar 2025 14:54:44 +0100 Subject: [PATCH 080/109] Added appearance settings. --- wobuzz/gui.py | 8 ++++++-- wobuzz/settings.py | 1 + wobuzz/ui/settings/settings.py | 26 ++++++++++++++++++++++++++ wobuzz/ui/track_info.py | 12 +++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index dcf53e6..e92ddac 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -58,8 +58,12 @@ class GUI: def on_settings_change(self, key, value): self.settings.update_settings(key, value) - if key == "gui_update_rate": - self.gui_update_timer.setInterval(1000 // value) + match key: + case "gui_update_rate": + self.gui_update_timer.setInterval(1000 // value) + + case "album_cover_size": + self.track_info.set_size(value) def on_track_change(self, previous_track, track): self.track_control.on_track_change(previous_track, track) diff --git a/wobuzz/settings.py b/wobuzz/settings.py index a2848c4..f13e0dd 100644 --- a/wobuzz/settings.py +++ b/wobuzz/settings.py @@ -12,4 +12,5 @@ class Settings: latest_playlist: str=None load_on_start: bool=False gui_update_rate: int=20 + album_cover_size: int=64 diff --git a/wobuzz/ui/settings/settings.py b/wobuzz/ui/settings/settings.py index b491835..74eaf18 100644 --- a/wobuzz/ui/settings/settings.py +++ b/wobuzz/ui/settings/settings.py @@ -68,6 +68,27 @@ class Settings(QDockWidget): self.tabs.addTab(self.behavior_settings, "Behavior") + self.appearance_settings = Category() + + self.appearance_settings.track_info = SubCategory( + "Track Info", + "Settings related to the appearance of the track info bar" + ) + self.appearance_settings.add_sub_category(self.appearance_settings.track_info) + + self.appearance_settings.track_info.cover_size = QSpinBox() + self.appearance_settings.track_info.cover_size.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.appearance_settings.track_info.cover_size.setRange(16, 128) + self.appearance_settings.track_info.cover_size.setSuffix("px") + self.appearance_settings.track_info.cover_size.setSingleStep(10) + self.appearance_settings.track_info.add_setting( + "Album Cover Size", + self.appearance_settings.track_info.cover_size, + "The size of the album cover. (aspect-ratio: 1:1)" + ) + + self.tabs.addTab(self.appearance_settings, "Appearance") + self.performance_settings = Category() # self.performance_settings.memory = SubCategory("Memory", "Memory related settings") @@ -103,6 +124,7 @@ class Settings(QDockWidget): self.behavior_settings.track.clear_cache.setChecked(self.app.settings.clear_track_cache) self.behavior_settings.playlist.load_on_start.setChecked(self.app.settings.load_on_start) self.performance_settings.cpu.gui_update_rate.setValue(self.app.settings.gui_update_rate) + self.appearance_settings.track_info.cover_size.setValue(self.app.settings.album_cover_size) def update_settings(self, key, value): match key: @@ -118,9 +140,13 @@ class Settings(QDockWidget): case "gui_update_rate": self.performance_settings.cpu.gui_update_rate.setValue(value) + case "track_cover_size": + self.appearance_settings.track_info.cover_size.setValue(value) + def write_settings(self): self.app.settings.library_path = self.file_settings.paths.library_path_input.text() self.app.settings.clear_track_cache = self.behavior_settings.track.clear_cache.isChecked() self.app.settings.load_on_start = self.behavior_settings.playlist.load_on_start.isChecked() self.app.settings.gui_update_rate = self.performance_settings.cpu.gui_update_rate.value() + self.app.settings.album_cover_size = self.appearance_settings.track_info.cover_size.value() diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index b37b9a3..e84be21 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -25,7 +25,8 @@ class TrackInfo(QToolBar): self.wobuzz_logo = QPixmap(f"{self.app.utils.wobuzz_location}/icon.svg") self.track_cover = QLabel(self) - self.track_cover.setFixedSize(64, 64) + self.track_cover.setMargin(4) + self.set_size(self.app.settings.album_cover_size) self.track_cover.setScaledContents(True) self.track_cover.setPixmap(self.wobuzz_logo) self.addWidget(self.track_cover) @@ -36,17 +37,24 @@ class TrackInfo(QToolBar): self.addWidget(self.info_container) self.title = QLabel("Title", self.info_container) + self.title.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum) self.title.setFont(self.title_font) info_container_layout.addWidget(self.title) self.artist = QLabel("Artist", self.info_container) + self.artist.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum) self.artist.setFont(self.artist_font) info_container_layout.addWidget(self.artist) self.album = QLabel("Album", self.info_container) + self.album.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum) self.album.setFont(self.album_font) info_container_layout.addWidget(self.album) + # spacer widget that makes the label spacing not depend on the container's height + spacer_widget = QWidget(self.info_container) + info_container_layout.addWidget(spacer_widget) + def update_info(self): current_playlist = self.app.player.current_playlist @@ -94,4 +102,6 @@ class TrackInfo(QToolBar): self.album.setText("") self.track_cover.setPixmap(self.wobuzz_logo) + def set_size(self, size: int): + self.track_cover.setFixedSize(size, size) From a4d1d31e0b6d498c785035001a843dc8f229ca0a Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 2 Mar 2025 16:57:04 +0100 Subject: [PATCH 081/109] Made the main Library not be a dock. --- wobuzz/gui.py | 11 +++-------- wobuzz/library/library.py | 20 ++++++++++---------- wobuzz/player/playlist.py | 2 +- wobuzz/ui/library.py | 4 ++-- wobuzz/ui/library_dock.py | 3 +-- wobuzz/ui/playlist.py | 6 +++--- wobuzz/ui/process/process_dock.py | 2 +- 7 files changed, 21 insertions(+), 27 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index e92ddac..d01a468 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -23,12 +23,7 @@ class GUI: self.popups = Popups(app, self) - self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock) - - self.app.library.main_library_dock.setFeatures( - QDockWidget.DockWidgetFeature.DockWidgetMovable | - QDockWidget.DockWidgetFeature.DockWidgetFloatable - ) + self.window.setCentralWidget(self.app.library.main_library_widget) if self.app.settings.window_maximized: self.window.showMaximized() @@ -68,8 +63,8 @@ class GUI: def on_track_change(self, previous_track, track): self.track_control.on_track_change(previous_track, track) - for dock_id in self.app.player.current_playlist.views: - view = self.app.player.current_playlist.views[dock_id] + for library_widget_id in self.app.player.current_playlist.views: + view = self.app.player.current_playlist.views[library_widget_id] view.on_track_change(previous_track, track) def on_background_job_start(self, job_name: str, description: str, steps: int=0, getter: any=None): diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 5693db0..1daf357 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -3,7 +3,7 @@ import os from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from ..player.playlist import Playlist -from ..ui.library_dock import LibraryDock +from ..ui.library import LibraryWidget from ..ui.playlist import PlaylistView @@ -15,8 +15,8 @@ class Library: def __init__(self, app): self.app = app - self.main_library_dock = LibraryDock(self) - self.library_docks = [self.main_library_dock] + self.main_library_widget = LibraryWidget(self) + self.library_widgets = [self.main_library_widget] self.playlists = [] self.temporary_playlist = None @@ -49,15 +49,15 @@ class Library: def load_playlist_views(self): # create views for each dock and playlist - for library_dock in self.library_docks: - playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs + for library_widget in self.library_widgets: + playlist_tabs: QTabWidget = library_widget.playlist_tabs # create view for each playlist for playlist in self.playlists: - if id(library_dock) in playlist.views: # view already exists + if id(library_widget) in playlist.views: # view already exists continue - playlist_view = PlaylistView(playlist, library_dock) + playlist_view = PlaylistView(playlist, library_widget) playlist_tabs.addTab(playlist_view, playlist.title) if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened and loaded @@ -83,10 +83,10 @@ class Library: self.playlists.append(playlist) - for library_dock in self.library_docks: - playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs + for library_widget in self.library_widgets: + playlist_tabs: QTabWidget = library_widget.playlist_tabs - playlist_view = PlaylistView(playlist, library_dock) + playlist_view = PlaylistView(playlist, library_widget) playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop playlist_tabs.addTab(playlist_view, playlist.title) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 790d86b..8f07228 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -34,7 +34,7 @@ class Playlist: self.tracks: list[Track] = [] self.current_track_index = 0 self.current_track: Track | None = None - self.views = {} # dict of id(LibraryDock): PlaylistView + self.views = {} # dict of id(LibraryWidget): PlaylistView self.loaded = False self.loading = False diff --git a/wobuzz/ui/library.py b/wobuzz/ui/library.py index a269b4b..c85bfaf 100644 --- a/wobuzz/ui/library.py +++ b/wobuzz/ui/library.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QToolBox, QLabel, QTabWidget, QToolButton +from PyQt6.QtWidgets import QToolBox, QLabel, QToolButton from .playlist_tabs import PlaylistTabs -class Library(QToolBox): +class LibraryWidget(QToolBox): def __init__(self, library, parent=None): super().__init__(parent) diff --git a/wobuzz/ui/library_dock.py b/wobuzz/ui/library_dock.py index d4694f5..c66f35d 100644 --- a/wobuzz/ui/library_dock.py +++ b/wobuzz/ui/library_dock.py @@ -1,8 +1,7 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QDockWidget -from .library import Library +from .library import LibraryWidget class LibraryDock(QDockWidget): diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist.py index 76ff343..acfbdcc 100644 --- a/wobuzz/ui/playlist.py +++ b/wobuzz/ui/playlist.py @@ -13,11 +13,11 @@ class PlaylistView(QTreeWidget): playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) - def __init__(self, playlist, dock, parent=None): + def __init__(self, playlist, library_widget, parent=None): super().__init__(parent) self.playlist = playlist - self.library_dock = dock + self.library_widget = library_widget self.app = playlist.app @@ -25,7 +25,7 @@ class PlaylistView(QTreeWidget): self.header.setSectionsClickable(True) self.header.setSortIndicatorShown(True) - playlist.views[id(dock)] = self # let the playlist know that this view exists + playlist.views[id(self.library_widget)] = self # let the playlist know that this view exists self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) diff --git a/wobuzz/ui/process/process_dock.py b/wobuzz/ui/process/process_dock.py index e106c59..a310fd4 100644 --- a/wobuzz/ui/process/process_dock.py +++ b/wobuzz/ui/process/process_dock.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -from PyQt6.QtCore import pyqtSignal +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout from .process import BackgroundProcess From 0929e38189586c65bf1a6e96a74edca760447376 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 2 Mar 2025 16:59:09 +0100 Subject: [PATCH 082/109] Added a window title to the track info. --- wobuzz/ui/track_info.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index e84be21..fb36cf2 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -20,6 +20,7 @@ class TrackInfo(QToolBar): self.app = app + self.setWindowTitle("Track Info") self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.wobuzz_logo = QPixmap(f"{self.app.utils.wobuzz_location}/icon.svg") From 2b239e57f06e32ea6fc7a5bb7b83ae99133c608b Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 2 Mar 2025 17:45:08 +0100 Subject: [PATCH 083/109] Made the view menu use the autogenerated QMainWindow.createPopupMenu(). --- wobuzz/gui.py | 2 -- wobuzz/ui/main_window.py | 13 +++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/wobuzz/gui.py b/wobuzz/gui.py index d01a468..8e2438c 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -11,8 +11,6 @@ class GUI: def __init__(self, app): self.app = app - - self.dropped = [] self.window = MainWindow(app, self) diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 738cb70..a36d576 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt, QPoint +from PyQt6.QtGui import QIcon, QContextMenuEvent from PyQt6.QtWidgets import QMainWindow, QMenu from .track_control import TrackControl from .settings import Settings @@ -37,13 +37,9 @@ class MainWindow(QMainWindow): self.edit_menu = QMenu("&Edit", self.menu_bar) self.menu_bar.addMenu(self.edit_menu) - self.settings_action = self.edit_menu.addAction("&Settings") - self.view_menu = QMenu("&View", self.menu_bar) self.menu_bar.addMenu(self.view_menu) - self.processes_action = self.view_menu.addAction("Show &Background Processes") - self.track_control = TrackControl(app) self.addToolBar(self.track_control) @@ -58,5 +54,6 @@ class MainWindow(QMainWindow): self.track_info = TrackInfo(app) self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.track_info) - self.settings_action.triggered.connect(self.settings.show) - self.processes_action.triggered.connect(self.process_dock.show) + dock_menu = self.createPopupMenu() + dock_menu.setTitle("Docks And Toolbars") + self.view_menu.addMenu(dock_menu) From 829dc05c49b3e2d3195e8207abaaba715e7872db Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 2 Mar 2025 17:53:04 +0100 Subject: [PATCH 084/109] Improved settings layout and deleted old file settings and behavior settings classes. --- wobuzz/ui/settings/behavior.py | 17 ----------------- wobuzz/ui/settings/file.py | 17 ----------------- wobuzz/ui/settings/settings.py | 18 ++++++++---------- wobuzz/ui/settings/sub_category.py | 9 +++++---- 4 files changed, 13 insertions(+), 48 deletions(-) delete mode 100644 wobuzz/ui/settings/behavior.py delete mode 100644 wobuzz/ui/settings/file.py diff --git a/wobuzz/ui/settings/behavior.py b/wobuzz/ui/settings/behavior.py deleted file mode 100644 index 12c32c5..0000000 --- a/wobuzz/ui/settings/behavior.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/python3 - -from PyQt6.QtWidgets import QWidget, QFormLayout, QCheckBox - - -class BehaviourSettings(QWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.layout = QFormLayout(self) - self.setLayout(self.layout) - - self.load_on_start = QCheckBox(self) - self.layout.addRow("Load playlists on start", self.load_on_start) - - self.clear_track_cache = QCheckBox(self) - self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache) diff --git a/wobuzz/ui/settings/file.py b/wobuzz/ui/settings/file.py deleted file mode 100644 index f380f94..0000000 --- a/wobuzz/ui/settings/file.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/python3 - -from PyQt6.QtGui import QPalette -from PyQt6.QtWidgets import QWidget, QLineEdit, QFormLayout - - -class FileSettings(QWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.layout = QFormLayout(self) - self.setLayout(self.layout) - - self.library_path_input = QLineEdit(self) - - self.layout.addRow("Library Path:", self.library_path_input) - diff --git a/wobuzz/ui/settings/settings.py b/wobuzz/ui/settings/settings.py index 74eaf18..5678b82 100644 --- a/wobuzz/ui/settings/settings.py +++ b/wobuzz/ui/settings/settings.py @@ -40,7 +40,7 @@ class Settings(QDockWidget): self.file_settings = Category() - self.file_settings.paths = SubCategory("Paths", "Path related settings") + self.file_settings.paths = SubCategory("Paths") self.file_settings.add_sub_category(self.file_settings.paths) self.file_settings.paths.library_path_input = QLineEdit() @@ -50,13 +50,13 @@ class Settings(QDockWidget): self.behavior_settings = Category() - self.behavior_settings.playlist = SubCategory("Playlist", "Playlist behavior") + self.behavior_settings.playlist = SubCategory("Playlist",) self.behavior_settings.add_sub_category(self.behavior_settings.playlist) self.behavior_settings.playlist.load_on_start = QCheckBox() self.behavior_settings.playlist.add_setting("Load on start:", self.behavior_settings.playlist.load_on_start) - self.behavior_settings.track = SubCategory("Track", "Track behavior") + self.behavior_settings.track = SubCategory("Track",) self.behavior_settings.add_sub_category(self.behavior_settings.track) self.behavior_settings.track.clear_cache = QCheckBox() @@ -70,10 +70,7 @@ class Settings(QDockWidget): self.appearance_settings = Category() - self.appearance_settings.track_info = SubCategory( - "Track Info", - "Settings related to the appearance of the track info bar" - ) + self.appearance_settings.track_info = SubCategory("Track Info") self.appearance_settings.add_sub_category(self.appearance_settings.track_info) self.appearance_settings.track_info.cover_size = QSpinBox() @@ -94,7 +91,7 @@ class Settings(QDockWidget): # self.performance_settings.memory = SubCategory("Memory", "Memory related settings") # self.performance_settings.add_sub_category(self.performance_settings.memory) - self.performance_settings.cpu = SubCategory("CPU", "CPU related settings") + self.performance_settings.cpu = SubCategory("CPU",) self.performance_settings.add_sub_category(self.performance_settings.cpu) self.performance_settings.cpu.gui_update_rate = QSpinBox() @@ -105,8 +102,9 @@ class Settings(QDockWidget): self.performance_settings.cpu.add_setting( "GUI update rate:", self.performance_settings.cpu.gui_update_rate, - "The rate at which gui-elements like the track-progress-slider get updated. Values above 20 " - "don't really make sense on most monitors. Decreasing this value will reduce the CPU usage." + "The rate at which gui-elements like the track-progress-slider get updated.\n" + "Values above 20 don't really make sense for most monitors.\n" + "Decreasing this value will reduce the CPU usage." ) self.tabs.addTab(self.performance_settings, "Performance") diff --git a/wobuzz/ui/settings/sub_category.py b/wobuzz/ui/settings/sub_category.py index 8ad34f5..694b682 100644 --- a/wobuzz/ui/settings/sub_category.py +++ b/wobuzz/ui/settings/sub_category.py @@ -9,7 +9,7 @@ class SubCategory(QGroupBox): description_font = QFont() description_font.setPointSize(8) - def __init__(self, title: str, description: str, parent=None): + def __init__(self, title: str, description: str=None, parent=None): super().__init__(title, parent) self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) @@ -20,14 +20,15 @@ class SubCategory(QGroupBox): self.layout = QFormLayout() self.setLayout(self.layout) - self.description = QLabel(description + "\n", self) - self.layout.addRow(self.description) + if description is not None: + self.description = QLabel(description + "\n", self) + self.layout.addRow(self.description) def add_setting(self, text: str, setting, description: str=None): self.layout.addRow(text, setting) if description is not None: - description_label = QLabel(" " + description) + description_label = QLabel(" " + description.replace("\n", "\n ")) description_label.setFont(self.description_font) self.layout.addRow(description_label) From 5f20c6e5b09face5e4233e8855a5e53ef6d945ff Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Mar 2025 15:02:14 +0100 Subject: [PATCH 085/109] Set parent parameter on playlist view creation because not setting it can sometimes cause bugs. --- wobuzz/library/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 1daf357..e89c95a 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -86,7 +86,7 @@ class Library: for library_widget in self.library_widgets: playlist_tabs: QTabWidget = library_widget.playlist_tabs - playlist_view = PlaylistView(playlist, library_widget) + playlist_view = PlaylistView(playlist, library_widget, playlist_tabs) playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop playlist_tabs.addTab(playlist_view, playlist.title) From 9ee4184c84c40638a3b48ee43cd71e0aaf2d539c Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Mar 2025 16:13:37 +0100 Subject: [PATCH 086/109] Added playlist tab title index synchronisation to fix some bugs. --- wobuzz/ui/playlist_tabs/tab_bar.py | 11 +++++++++++ wobuzz/ui/playlist_tabs/tab_widget.py | 15 +++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py index 8519ffd..be7621e 100644 --- a/wobuzz/ui/playlist_tabs/tab_bar.py +++ b/wobuzz/ui/playlist_tabs/tab_bar.py @@ -19,6 +19,7 @@ class PlaylistTabBar(QTabBar): self.tabBarClicked.connect(self.on_click) self.tabBarDoubleClicked.connect(self.on_doubleclick) + self.tabMoved.connect(self.on_tab_move) def dragEnterEvent(self, event: QDragEnterEvent): index = self.tabAt(event.position().toPoint()) @@ -62,3 +63,13 @@ class PlaylistTabBar(QTabBar): self.context_menu.exec(event.globalPos(), title) + def on_tab_move(self, i_from, i_to): + title = self.tabButton(i_to, QTabBar.ButtonPosition.RightSide) + + # update the index + title.index = i_to + + def update_title_indexes(self, after: int): + for i in range(after, self.count()): + title = self.tabButton(i, QTabBar.ButtonPosition.RightSide) + title.index = i diff --git a/wobuzz/ui/playlist_tabs/tab_widget.py b/wobuzz/ui/playlist_tabs/tab_widget.py index afdb04b..d2ffce6 100644 --- a/wobuzz/ui/playlist_tabs/tab_widget.py +++ b/wobuzz/ui/playlist_tabs/tab_widget.py @@ -20,12 +20,19 @@ class PlaylistTabs(QTabWidget): self.setMovable(True) self.setAcceptDrops(True) - def addTab(self, playlist_view, label): - super().addTab(playlist_view, None) - - index = self.tab_bar.count() - 1 + def addTab(self, playlist_view, label) -> int: + index = super().addTab(playlist_view, None) title = TabTitle(self.app, label, self.tab_bar, index, playlist_view) self.tab_bar.setTabButton(index, QTabBar.ButtonPosition.RightSide, title) + return index + + def tabRemoved(self, index): + # Update indexes because when a playlist is replaced, (and the old playlist widget is deleted by deleteLater()) + # the old playlist_widget is actually deleted later than the new one is created. + # Because of this, the new playlist tab gets immediately moved one to the left and we have to update the + # indexes. + self.tab_bar.update_title_indexes(index) + From 0101cf174cf3eb17aa618fa60fd84da2376be094 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Mar 2025 16:21:43 +0100 Subject: [PATCH 087/109] Added "Open Playlist" option to the menubar. --- wobuzz/library/library.py | 8 ++++---- wobuzz/player/playlist.py | 2 +- wobuzz/ui/main_window.py | 3 ++- wobuzz/ui/popups.py | 8 ++++++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index e89c95a..6e4eb1d 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -95,9 +95,6 @@ class Library: if self.temporary_playlist is not None: self.temporary_playlist.delete() - if self.temporary_playlist in self.playlists: - self.playlists.remove(self.temporary_playlist) - if not replace in self.playlists: self.playlists.append(replace) @@ -112,7 +109,7 @@ class Library: playlist.load() - def import_playlist(self, playlist_path: str): + def open_playlist(self, playlist_path: str): playlist = Playlist(self.app, "Temporary Playlist", playlist_path) self.replace_temporary_playlist(playlist) @@ -121,3 +118,6 @@ class Library: playlist.load() + def import_playlist(self, playlist_path: str): + self.open_playlist(playlist_path) + diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 8f07228..b1d4228 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -317,7 +317,7 @@ class Playlist: track.delete_items(self) # make sure the playlist is not referenced as the temporary playlist - if self == self.app.library.temporary_playlist: + if self is self.app.library.temporary_playlist: self.app.library.temporary_playlist = None self.app.utils.unique_names.remove(self.title) diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index a36d576..f3036bb 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -32,7 +32,8 @@ class MainWindow(QMainWindow): self.playlist_menu = QMenu("&Playlist", self.menu_bar) self.menu_bar.addMenu(self.playlist_menu) - self.import_playlist_action = self.playlist_menu.addAction("&Import Playlists") + self.open_playlist_action = self.playlist_menu.addAction("&Open Playlist") + self.import_playlist_action = self.playlist_menu.addAction("&Import Playlist") self.edit_menu = QMenu("&Edit", self.menu_bar) self.menu_bar.addMenu(self.edit_menu) diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py index a30d539..1b54bb4 100644 --- a/wobuzz/ui/popups.py +++ b/wobuzz/ui/popups.py @@ -22,6 +22,7 @@ class Popups: self.window.open_track_action.triggered.connect(self.open_tracks) self.window.import_track_action.triggered.connect(self.import_tracks) + self.window.open_playlist_action.triggered.connect(self.open_playlist) self.window.import_playlist_action.triggered.connect(self.import_playlist) def select_audio_files(self): @@ -41,8 +42,11 @@ class Popups: def import_tracks(self): self.open_tracks() # placeholder - def import_playlist(self): + def open_playlist(self): playlist_path = self.select_playlist_file() if playlist_path is not None and not playlist_path == "": - self.app.library.import_playlist(playlist_path) \ No newline at end of file + self.app.library.open_playlist(playlist_path) + + def import_playlist(self): + self.open_playlist() # placeholder From 7ff1ad7a0298c57b345901f15ac9bce23148d06e Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Mon, 3 Mar 2025 17:18:30 +0100 Subject: [PATCH 088/109] Made the playlist tabs look prettier by making the tab title editor non-static. --- wobuzz/gui.py | 2 + wobuzz/player/playlist.py | 4 +- wobuzz/ui/playlist_tabs/tab_bar.py | 17 ++------- wobuzz/ui/playlist_tabs/tab_context_menu.py | 19 +++++++--- wobuzz/ui/playlist_tabs/tab_title.py | 42 --------------------- wobuzz/ui/playlist_tabs/tab_title_editor.py | 26 +++++++++++++ wobuzz/ui/playlist_tabs/tab_widget.py | 20 +--------- 7 files changed, 46 insertions(+), 84 deletions(-) delete mode 100644 wobuzz/ui/playlist_tabs/tab_title.py create mode 100644 wobuzz/ui/playlist_tabs/tab_title_editor.py diff --git a/wobuzz/gui.py b/wobuzz/gui.py index 8e2438c..c23dc49 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -40,6 +40,8 @@ class GUI: self.settings.update_all() def on_exit(self, event): + self.window.focusWidget().clearFocus() # clear focus on focused widget + self.app.player.stop() self.app.library.on_exit(event) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index b1d4228..3cb4388 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -265,7 +265,6 @@ class Playlist: return self.current_track.sound, self.current_track.duration def save(self): - first_view = list(self.views.values())[0] first_view.sortItems(4, Qt.SortOrder.AscendingOrder) self.sync(first_view) @@ -283,8 +282,6 @@ class Playlist: wbz.close() def rename(self, title: str): - # remove from unique names so a new playlist can have the old name and delete old playlist. - if os.path.exists(self.path): os.remove(self.path) @@ -299,6 +296,7 @@ class Playlist: if self == self.app.library.temporary_playlist: self.app.library.temporary_playlist = None + # remove from unique names so a new playlist can have the old name and delete old playlist. if not old_title == self.title: # remove only when the playlist actually has a different name self.app.utils.unique_names.remove(old_title) diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py index be7621e..ed50ba8 100644 --- a/wobuzz/ui/playlist_tabs/tab_bar.py +++ b/wobuzz/ui/playlist_tabs/tab_bar.py @@ -19,7 +19,6 @@ class PlaylistTabBar(QTabBar): self.tabBarClicked.connect(self.on_click) self.tabBarDoubleClicked.connect(self.on_doubleclick) - self.tabMoved.connect(self.on_tab_move) def dragEnterEvent(self, event: QDragEnterEvent): index = self.tabAt(event.position().toPoint()) @@ -59,17 +58,7 @@ class PlaylistTabBar(QTabBar): if index == -1: # when no tab was clicked, do nothing return - title = self.tabButton(index, QTabBar.ButtonPosition.RightSide) + playlist_view = self.tab_widget.widget(index) + playlist = playlist_view.playlist - self.context_menu.exec(event.globalPos(), title) - - def on_tab_move(self, i_from, i_to): - title = self.tabButton(i_to, QTabBar.ButtonPosition.RightSide) - - # update the index - title.index = i_to - - def update_title_indexes(self, after: int): - for i in range(after, self.count()): - title = self.tabButton(i, QTabBar.ButtonPosition.RightSide) - title.index = i + self.context_menu.exec(event.globalPos(), index, playlist) diff --git a/wobuzz/ui/playlist_tabs/tab_context_menu.py b/wobuzz/ui/playlist_tabs/tab_context_menu.py index 12ec0df..6d78b80 100644 --- a/wobuzz/ui/playlist_tabs/tab_context_menu.py +++ b/wobuzz/ui/playlist_tabs/tab_context_menu.py @@ -4,6 +4,8 @@ from PyQt6.QtCore import QPoint from PyQt6.QtGui import QAction from PyQt6.QtWidgets import QMenu, QTabBar +from .tab_title_editor import TabTitleEditor + class PlaylistContextMenu(QMenu): def __init__(self, parent=None): @@ -11,7 +13,8 @@ class PlaylistContextMenu(QMenu): self.tab_bar: QTabBar = parent - self.playlist_title = None + self.tab_index = -1 + self.playlist = None self.title = self.addSection("Playlist Actions") @@ -24,15 +27,19 @@ class PlaylistContextMenu(QMenu): self.rename_action.triggered.connect(self.rename) self.delete_action.triggered.connect(self.delete) - def exec(self, pos: QPoint, title): - self.playlist_title = title + def exec(self, pos: QPoint, index: int, playlist): + self.tab_index = index + self.playlist = playlist - self.title.setText(title.text()) # set section title + self.title.setText(playlist.title) super().exec(pos) def rename(self): - self.playlist_title.setFocus() + # create temporary QLineEdit for renaming the tab + title_editor = TabTitleEditor(self.playlist, self.tab_bar, self.tab_index) + + self.tab_bar.setTabButton(self.tab_index, QTabBar.ButtonPosition.RightSide, title_editor) def delete(self): - self.playlist_title.playlist_view.playlist.delete() + self.playlist.delete() diff --git a/wobuzz/ui/playlist_tabs/tab_title.py b/wobuzz/ui/playlist_tabs/tab_title.py deleted file mode 100644 index 5ae4181..0000000 --- a/wobuzz/ui/playlist_tabs/tab_title.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/python3 - -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QMouseEvent, QCursor -from PyQt6.QtWidgets import QLineEdit - -from .tab_bar import PlaylistTabBar - - -class TabTitle(QLineEdit): - def __init__(self, app, label, parent, index: int, playlist_view): - super().__init__(label, parent) - - self.app = app - self.tab_bar: PlaylistTabBar = parent - self.index = index - self.playlist_view = playlist_view - - self.setStyleSheet("QLineEdit {background: transparent; border: none;}") - self.setFocusPolicy(Qt.FocusPolicy.TabFocus) - - self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor) - - self.editingFinished.connect(self.on_edit) - - def mouseDoubleClickEvent(self, event: QMouseEvent): - self.tab_bar.tabBarDoubleClicked.emit(self.index) - - def mousePressEvent(self, event: QMouseEvent): - self.tab_bar.tabBarClicked.emit(self.index) - self.tab_bar.setCurrentIndex(self.index) - - def contextMenuEvent(self, event): - self.tab_bar.contextMenuEvent(event, self) - - def on_edit(self): - self.clearFocus() - - self.playlist_view.playlist.rename(self.text()) - - self.setText(self.playlist_view.playlist.title) - diff --git a/wobuzz/ui/playlist_tabs/tab_title_editor.py b/wobuzz/ui/playlist_tabs/tab_title_editor.py new file mode 100644 index 0000000..3b020d7 --- /dev/null +++ b/wobuzz/ui/playlist_tabs/tab_title_editor.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 + +from PyQt6.QtWidgets import QLineEdit, QTabBar + + +class TabTitleEditor(QLineEdit): + def __init__(self, playlist, parent, index: int): + super().__init__(playlist.title, parent) + + self.playlist = playlist + self.tab_bar = parent + self.index = index + + self.tab_bar.setTabText(index, "") + + self.setFocus() + + self.editingFinished.connect(self.on_edit) + + def on_edit(self): + self.playlist.rename(self.text()) + + self.deleteLater() + self.tab_bar.setTabButton(self.index, QTabBar.ButtonPosition.RightSide, None) + self.tab_bar.setTabText(self.index, self.playlist.title) + diff --git a/wobuzz/ui/playlist_tabs/tab_widget.py b/wobuzz/ui/playlist_tabs/tab_widget.py index d2ffce6..4976458 100644 --- a/wobuzz/ui/playlist_tabs/tab_widget.py +++ b/wobuzz/ui/playlist_tabs/tab_widget.py @@ -1,9 +1,8 @@ #!/usr/bin/python3 -from PyQt6.QtWidgets import QTabWidget, QTabBar +from PyQt6.QtWidgets import QTabWidget from .tab_bar import PlaylistTabBar -from .tab_title import TabTitle class PlaylistTabs(QTabWidget): @@ -19,20 +18,3 @@ class PlaylistTabs(QTabWidget): self.setMovable(True) self.setAcceptDrops(True) - - def addTab(self, playlist_view, label) -> int: - index = super().addTab(playlist_view, None) - - title = TabTitle(self.app, label, self.tab_bar, index, playlist_view) - - self.tab_bar.setTabButton(index, QTabBar.ButtonPosition.RightSide, title) - - return index - - def tabRemoved(self, index): - # Update indexes because when a playlist is replaced, (and the old playlist widget is deleted by deleteLater()) - # the old playlist_widget is actually deleted later than the new one is created. - # Because of this, the new playlist tab gets immediately moved one to the left and we have to update the - # indexes. - self.tab_bar.update_title_indexes(index) - From 072f5c769182122d8d1016de66114c12a2892929 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Tue, 4 Mar 2025 17:24:24 +0100 Subject: [PATCH 089/109] Created own metadata dataclass instead of using TinyTag.tags() --- wobuzz/player/track.py | 30 +++++++++++++++++++++++------- wobuzz/ui/track.py | 6 +++--- wobuzz/ui/track_info.py | 12 ++++++------ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index bf74933..0b11411 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -3,18 +3,35 @@ from pydub import AudioSegment from pygame.mixer import Sound from tinytag import TinyTag +from tinytag.tinytag import Images as TTImages +from dataclasses import dataclass + + +@dataclass +class TrackMetadata: + title: str + artist: str + album: str + images: TTImages | None=None # tinytag images class Track: """ - Class containing data for a track like file path, raw data... + Class representing a track. """ - def __init__(self, app, path: str, cache: bool=False): + def __init__(self, app, path: str, cache: bool=False, metadata: TrackMetadata=None): self.app = app self.path = path - self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) + if metadata is None: + # load metadata from audio file + tags = TinyTag.get(self.path, ignore_errors=True, duration=False) + + self.metadata = TrackMetadata(tags.title, tags.artist, tags.album) + + else: + self.metadata = metadata self.cached = False self.audio = None @@ -67,7 +84,8 @@ class Track: self.duration = len(self.audio) # track duration in milliseconds - self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images + # metadata with images + self.metadata.images = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True).images self.cached = True @@ -78,11 +96,9 @@ class Track: self.sound = None self.duration = 0 - self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images + self.metadata.images = None def load_audio(self): - #file_type = self.path.split(".")[-1] - self.audio = AudioSegment.from_file(self.path) def remaining(self, position: int): diff --git a/wobuzz/ui/track.py b/wobuzz/ui/track.py index e1c0f24..f67e21e 100644 --- a/wobuzz/ui/track.py +++ b/wobuzz/ui/track.py @@ -38,9 +38,9 @@ class TrackItem(QTreeWidgetItem): ) self.setText(0, str(self.index + 1)) - self.setText(1, track.tags.title) - self.setText(2, track.tags.artist) - self.setText(3, track.tags.album) + self.setText(1, track.metadata.title) + self.setText(2, track.metadata.artist) + self.setText(3, track.metadata.album) self.setText(4, str(self.index_user_sort + 1)) def mark(self): diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index fb36cf2..41dd515 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -61,25 +61,25 @@ class TrackInfo(QToolBar): if current_playlist is not None and current_playlist.current_track is not None: current_track = current_playlist.current_track - title = current_track.tags.title - artist = current_track.tags.artist - album = current_track.tags.album + title = current_track.metadata.title + artist = current_track.metadata.artist + album = current_track.metadata.album self.title.setText(title) if artist is not None and not artist == "": - self.artist.setText(f"By {current_track.tags.artist}") + self.artist.setText(f"By {artist}") else: self.artist.setText("No Artist") if album is not None and not album == "": - self.album.setText(f"In {current_track.tags.album}") + self.album.setText(f"In {album}") else: self.album.setText("No Album") - cover = current_track.tags.images.any + cover = current_track.metadata.images.any if cover is None: self.track_cover.setPixmap(self.wobuzz_logo) From 971ead90c1a03141082ee6429863e84be57adf66 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Tue, 4 Mar 2025 19:29:24 +0100 Subject: [PATCH 090/109] Implemented caching of title, artist and album of a track as WOBUZZM3U parameters. --- wobuzz/player/playlist.py | 41 ++++++++++++++++++++++++++++++--------- wobuzz/player/track.py | 32 +++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 3cb4388..c93ba9e 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -5,7 +5,7 @@ import threading from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QAbstractItemView -from .track import Track +from .track import Track, TrackMetadata class Playlist: @@ -38,9 +38,7 @@ class Playlist: self.loaded = False self.loading = False - self.path = os.path.expanduser( - f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" - ) + self.path = self.path_from_title(title) def clear(self): self.sorting: list[Qt.SortOrder] | None = None @@ -162,6 +160,8 @@ class Playlist: lambda: i ) + track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U + while i < num_lines: line = lines[i] @@ -177,16 +177,30 @@ class Playlist: # convert these from strings back to int and bool and append them to the sorting self.sorting.append((int(sorting[0]), sorting[1] == "True")) + elif line.startswith("#TRACK_TITLE: "): + track_metadata.title = line[14:] + + elif line.startswith("#TRACK_ARTIST: "): + track_metadata.artist = line[15:] + + elif line.startswith("#TRACK_ALBUM: "): + track_metadata.album = line[14:] + i += 1 continue - elif line.startswith("http"): # filter out urls + elif line.startswith("http"): # ignore urls i += 1 continue - self.append_track(Track(self.app, line, cache=i == 0)) # first track is cached + track_metadata.path = line + track_metadata.add_missing() + + self.append_track(Track(self.app, line, cache=i == 0, metadata=track_metadata)) # first track is cached + + track_metadata = TrackMetadata() # metadata for next track i += 1 @@ -275,6 +289,11 @@ class Playlist: wbz_data += f"#SORT: {sort_column}, {order}\n" for track in self.tracks: + # cache track metadata + wbz_data += f"#TRACK_TITLE: {track.metadata.title}\n" + wbz_data += f"#TRACK_ARTIST: {track.metadata.artist}\n" + wbz_data += f"#TRACK_ALBUM: {track.metadata.album}\n" + wbz_data += f"{track.path}\n" wbz = open(self.path, "w") @@ -288,9 +307,7 @@ class Playlist: old_title = self.title self.title = self.app.utils.unique_name(title, ignore=old_title) - self.path = os.path.expanduser( - f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u" - ) + self.path = self.path_from_title(self.title) # make sure the playlist is not referenced anymore as the temporary playlist if self == self.app.library.temporary_playlist: @@ -334,3 +351,9 @@ class Playlist: if len(self.tracks) > 1: return self.tracks[-2] + def path_from_title(self, title): + path = os.path.expanduser( + f"{self.app.settings.library_path}/playlists/{title.replace(" ", "_")}.wbz.m3u" + ) + + return path diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 0b11411..66bd26a 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -9,11 +9,33 @@ from dataclasses import dataclass @dataclass class TrackMetadata: - title: str - artist: str - album: str + path: str | None=None + title: str | None=None + artist: str | None=None + album: str | None=None images: TTImages | None=None # tinytag images + def add_missing(self): + # Make the album be an empty string instead of "None" + if self.title == "None": + self.title = "" + + if self.artist == "None": + self.artist = "" + + if self.album == "None": + self.album = "" + + if self.path is None: # can't add missing information without a path + return + + if self.title is None or self.artist is None or self.album is None: + tags = TinyTag.get(self.path, ignore_errors=True, duration=False) + + self.title = tags.title + self.artist = tags.artist + self.album = tags.album + class Track: """ @@ -26,9 +48,9 @@ class Track: if metadata is None: # load metadata from audio file - tags = TinyTag.get(self.path, ignore_errors=True, duration=False) + tags = TinyTag.get(path, ignore_errors=True, duration=False) - self.metadata = TrackMetadata(tags.title, tags.artist, tags.album) + self.metadata = TrackMetadata(path, tags.title, tags.artist, tags.album) else: self.metadata = metadata From 9ae1704e4aad8b8020a440831addb1953ec51ae1 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 5 Mar 2025 16:51:50 +0100 Subject: [PATCH 091/109] Fixed another missing NoneType-check that caused a crash. --- wobuzz/ui/track_info.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py index 41dd515..0be8010 100644 --- a/wobuzz/ui/track_info.py +++ b/wobuzz/ui/track_info.py @@ -71,17 +71,22 @@ class TrackInfo(QToolBar): self.artist.setText(f"By {artist}") else: - self.artist.setText("No Artist") + self.artist.setText("") if album is not None and not album == "": self.album.setText(f"In {album}") else: - self.album.setText("No Album") + self.album.setText("") + + if current_track.metadata.images is None: # can't display cover image when there are no images at all + self.track_cover.setPixmap(self.wobuzz_logo) + + return cover = current_track.metadata.images.any - if cover is None: + if cover is None: # can't display cover image when there is none self.track_cover.setPixmap(self.wobuzz_logo) return From 7edaebc3c332c718969bb41aadb176b20f116925 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 6 Mar 2025 16:35:13 +0100 Subject: [PATCH 092/109] Did some memory optimisation, moved some files and created a completely not tested gui class that will list an artist's tracks. Made tracks return an already existing object when they get created with a path of an already existing track object. --- wobuzz/library/library.py | 19 +++++++++- wobuzz/player/track.py | 15 ++++++++ wobuzz/ui/library/__init__.py | 1 + wobuzz/ui/library/artist_view.py | 42 +++++++++++++++++++++ wobuzz/ui/{ => library}/library.py | 2 +- wobuzz/ui/{ => library}/library_dock.py | 2 +- wobuzz/ui/{playlist.py => playlist_view.py} | 0 7 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 wobuzz/ui/library/__init__.py create mode 100644 wobuzz/ui/library/artist_view.py rename wobuzz/ui/{ => library}/library.py (94%) rename wobuzz/ui/{ => library}/library_dock.py (88%) rename wobuzz/ui/{playlist.py => playlist_view.py} (100%) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 6e4eb1d..cae70dd 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -2,9 +2,10 @@ import os from PyQt6.QtWidgets import QTabWidget, QAbstractItemView + from ..player.playlist import Playlist -from ..ui.library import LibraryWidget -from ..ui.playlist import PlaylistView +from ..ui.library.library import LibraryWidget +from ..ui.playlist_view import PlaylistView class Library: @@ -18,9 +19,13 @@ class Library: self.main_library_widget = LibraryWidget(self) self.library_widgets = [self.main_library_widget] + self.loaded_tracks = {} # dict of {track path: track} + self.playlists = [] self.temporary_playlist = None + self.artist_playlists = [] + def load(self): self.load_playlists() @@ -77,6 +82,8 @@ class Library: if self.app.player.current_playlist is not None: self.app.settings.latest_playlist = self.app.player.current_playlist.path + print(self.loaded_tracks) + def new_playlist(self): playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist")) playlist.loaded = True @@ -121,3 +128,11 @@ class Library: def import_playlist(self, playlist_path: str): self.open_playlist(playlist_path) + def loaded_track(self, track_path: str): + """ + Returns either a loaded track with the given path or None if there is none. + """ + + if track_path in self.loaded_tracks: + return self.loaded_tracks[track_path] + diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 66bd26a..09d497d 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -46,6 +46,9 @@ class Track: self.app = app self.path = path + # add self to loaded tracks to make sure that no other track object is created for this track + app.library.loaded_tracks[self.path] = self + if metadata is None: # load metadata from audio file tags = TinyTag.get(path, ignore_errors=True, duration=False) @@ -66,6 +69,18 @@ class Track: if cache: self.cache() + def __new__(cls, app, path: str, cache: bool=False, metadata: TrackMetadata=None): + loaded_track = app.library.loaded_track(path) + + if loaded_track is not None: + if cache: + loaded_track.cache() + + return loaded_track + + else: + return super().__new__(cls) + def set_occurrences(self): # set track item for every occurrence of track in a playlist diff --git a/wobuzz/ui/library/__init__.py b/wobuzz/ui/library/__init__.py new file mode 100644 index 0000000..a93a4bf --- /dev/null +++ b/wobuzz/ui/library/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/wobuzz/ui/library/artist_view.py b/wobuzz/ui/library/artist_view.py new file mode 100644 index 0000000..a444491 --- /dev/null +++ b/wobuzz/ui/library/artist_view.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView + +from ..playlist_view import PlaylistView + + +class ArtistView(PlaylistView): + def __init__(self, playlist, library_widget, parent=None): + QTreeWidget.__init__(self, parent) + + self.playlist = playlist + self.library_widget = library_widget + + self.app = playlist.app + + self.header = self.header() + self.header.setSectionsClickable(True) + self.header.setSortIndicatorShown(True) + + playlist.views[id(self.library_widget)] = self # let the playlist know that this view exists + + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + + self.setColumnCount(3) + + headers = [ + "#", + "Title", + "Artist", + "Album", + ] + + self.setHeaderLabels(headers) + + self.itemActivated.connect(self.on_track_activation) + self.header.sectionClicked.connect(self.on_header_click) + self.sort_signal.connect(self.sortItems) + + def setDragDropMode(self, behavior): + pass # user should not be able to sort the playlist manually + diff --git a/wobuzz/ui/library.py b/wobuzz/ui/library/library.py similarity index 94% rename from wobuzz/ui/library.py rename to wobuzz/ui/library/library.py index c85bfaf..302ac77 100644 --- a/wobuzz/ui/library.py +++ b/wobuzz/ui/library/library.py @@ -2,7 +2,7 @@ from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QToolBox, QLabel, QToolButton -from .playlist_tabs import PlaylistTabs +from wobuzz.ui.playlist_tabs import PlaylistTabs class LibraryWidget(QToolBox): diff --git a/wobuzz/ui/library_dock.py b/wobuzz/ui/library/library_dock.py similarity index 88% rename from wobuzz/ui/library_dock.py rename to wobuzz/ui/library/library_dock.py index c66f35d..f1362de 100644 --- a/wobuzz/ui/library_dock.py +++ b/wobuzz/ui/library/library_dock.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from PyQt6.QtWidgets import QDockWidget -from .library import LibraryWidget +from wobuzz.ui.library import LibraryWidget class LibraryDock(QDockWidget): diff --git a/wobuzz/ui/playlist.py b/wobuzz/ui/playlist_view.py similarity index 100% rename from wobuzz/ui/playlist.py rename to wobuzz/ui/playlist_view.py From f7995aee9e21975d8bc2e04f60172cdd29da1d30 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 6 Mar 2025 16:41:02 +0100 Subject: [PATCH 093/109] Removed a debug print. (Oops) --- wobuzz/library/library.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index cae70dd..21ebad0 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -82,8 +82,6 @@ class Library: if self.app.player.current_playlist is not None: self.app.settings.latest_playlist = self.app.player.current_playlist.path - print(self.loaded_tracks) - def new_playlist(self): playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist")) playlist.loaded = True From 9e20e21e6f35df88775f16b6901331b5d020ab82 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 7 Mar 2025 18:59:51 +0100 Subject: [PATCH 094/109] Added tools for reading and writing WOBUZZM3U and made the sorting parameters human readable. --- wobuzz/player/playlist.py | 65 +++++++++++++----------- wobuzz/ui/playlist_view.py | 26 +++++----- wobuzz/wobuzzm3u/__init__.py | 13 +++++ wobuzz/wobuzzm3u/wbzm3u_data.py | 68 ++++++++++++++++++++++++++ wobuzz/wobuzzm3u/wobuzzm3u.py | 87 +++++++++++++++++++++++++++++++++ 5 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 wobuzz/wobuzzm3u/__init__.py create mode 100644 wobuzz/wobuzzm3u/wbzm3u_data.py create mode 100644 wobuzz/wobuzzm3u/wobuzzm3u.py diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index c93ba9e..694ee4c 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -5,7 +5,10 @@ import threading from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QAbstractItemView +from reactivex.observable.case import case_ + from .track import Track, TrackMetadata +from ..wobuzzm3u import WobuzzM3U, WBZM3UData class Playlist: @@ -22,14 +25,13 @@ class Playlist: # no other playlist can be created using the same name self.app.utils.unique_names.append(self.title) - # the number is the index of the header section, - # the bool is the sorting order (True = ascending, False = descending) - self.sorting: list[tuple[int, bool]] = [ - (0, True), - (1, True), - (2, True), - (3, True), - (4, True) + # sort order + self.sorting: list[WBZM3UData.SortOrder] = [ + WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_title, True), + WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_artist, True), + WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_album, True), + WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_genre, True), + WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True) ] self.tracks: list[Track] = [] self.current_track_index = 0 @@ -160,37 +162,39 @@ class Playlist: lambda: i ) + wbzm3u = WobuzzM3U(self.path) track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U while i < num_lines: line = lines[i] - if line.startswith("#"): # comments and EXTM3U/WOBUZZM3U - if line.startswith("#SORT: "): # sort - sort_line = line[6:] # delete "#SORT: " from the line + line_data = wbzm3u.parse_line(line) - sorting = sort_line.split(", ") # split into the sort column specifier and the sort order - # e.g. ["0", "True"] + if line_data is None: + i += 1 + continue + + if line_data.is_comment: # comments and EXTM3U/WOBUZZM3U + if isinstance(line_data, WBZM3UData.SortOrder): # sort del self.sorting[0] # delete first sort so the length stays at 6 - # convert these from strings back to int and bool and append them to the sorting - self.sorting.append((int(sorting[0]), sorting[1] == "True")) + self.sorting.append(line_data) - elif line.startswith("#TRACK_TITLE: "): - track_metadata.title = line[14:] + if isinstance(line_data, WBZM3UData.TrackMetadata.TrackTitle): + track_metadata.title = line_data - elif line.startswith("#TRACK_ARTIST: "): - track_metadata.artist = line[15:] + if isinstance(line_data, WBZM3UData.TrackMetadata.TrackArtist): + track_metadata.artist = line_data - elif line.startswith("#TRACK_ALBUM: "): - track_metadata.album = line[14:] + if isinstance(line_data, WBZM3UData.TrackMetadata.TrackAlbum): + track_metadata.album = line_data i += 1 continue - elif line.startswith("http"): # ignore urls + elif isinstance(line_data, WBZM3UData.URL): # ignore urls i += 1 continue @@ -283,18 +287,21 @@ class Playlist: first_view.sortItems(4, Qt.SortOrder.AscendingOrder) self.sync(first_view) - wbz_data = "#WOBUZZM3U\n" # header + wbzm3u = WobuzzM3U(self.path) - for sort_column, order in self.sorting: - wbz_data += f"#SORT: {sort_column}, {order}\n" + wbz_data = "" + + for order in self.sorting: + wbz_data += wbzm3u.assemble_line(order) for track in self.tracks: # cache track metadata - wbz_data += f"#TRACK_TITLE: {track.metadata.title}\n" - wbz_data += f"#TRACK_ARTIST: {track.metadata.artist}\n" - wbz_data += f"#TRACK_ALBUM: {track.metadata.album}\n" + wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackTitle(track.metadata.title)) + wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackArtist(track.metadata.artist)) + wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackAlbum(track.metadata.album)) + # wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackGenre(track.metadata.genre)) - wbz_data += f"{track.path}\n" + wbz_data += wbzm3u.assemble_line(WBZM3UData.Path(track.path)) wbz = open(self.path, "w") wbz.write(wbz_data) diff --git a/wobuzz/ui/playlist_view.py b/wobuzz/ui/playlist_view.py index acfbdcc..6f09edd 100644 --- a/wobuzz/ui/playlist_view.py +++ b/wobuzz/ui/playlist_view.py @@ -5,6 +5,7 @@ from PyQt6.QtGui import QDropEvent, QIcon from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView from .track import TrackItem +from ..wobuzzm3u import WBZM3UData class PlaylistView(QTreeWidget): @@ -50,34 +51,36 @@ class PlaylistView(QTreeWidget): return sorting = self.playlist.sorting - last_sort_section_index, order = sorting[4] + last_order = sorting[4] - if last_sort_section_index == section_index: - order = not order # invert order + if last_order.sort_by + 1 == section_index: + order = WBZM3UData.SortOrder(last_order.sort_by, not last_order.ascending) # invert order on 2nd click - self.playlist.sorting[4] = (section_index, order) # set sorting + self.playlist.sorting[4] = order # set sorting # convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder - qorder = Qt.SortOrder.AscendingOrder if order else Qt.SortOrder.DescendingOrder + qorder = Qt.SortOrder.AscendingOrder if order.ascending else Qt.SortOrder.DescendingOrder self.header.setSortIndicator(section_index, qorder) else: del sorting[0] # remove first sort - sorting.append((section_index, True)) # last sort is this section index, ascending + + # last sort is this section index + 1, ascending + sorting.append(WBZM3UData.SortOrder(section_index - 1, True)) self.header.setSortIndicator(section_index, Qt.SortOrder.AscendingOrder) self.sort() def sort(self): - for index, order in self.playlist.sorting: + for order in self.playlist.sorting: # convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder - qorder = Qt.SortOrder.AscendingOrder if order else Qt.SortOrder.DescendingOrder + qorder = Qt.SortOrder.AscendingOrder if order.ascending else Qt.SortOrder.DescendingOrder # somehow, QTreeWidget.sortItems() cant be called from a thread, so we have to use a signal to execute it # in the main thread - self.sort_signal.emit(index, qorder) + self.sort_signal.emit(order.sort_by + 1, qorder) # self.sortItems(index, qorder) self.on_sort() @@ -98,10 +101,11 @@ class PlaylistView(QTreeWidget): track.setText(4, str(i)) # 4 = user sort index if user_sort: - if not self.playlist.sorting[4][0] == 4: # set last sort to user sort + # set last sort to user sort + if not self.playlist.sorting[4].sort_by == WBZM3UData.SortOrder.custom_sorting: del self.playlist.sorting[0] - self.playlist.sorting.append((4, True)) + self.playlist.sorting.append(WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True)) self.header.setSortIndicator(4, Qt.SortOrder.AscendingOrder) diff --git a/wobuzz/wobuzzm3u/__init__.py b/wobuzz/wobuzzm3u/__init__.py new file mode 100644 index 0000000..54b7ee8 --- /dev/null +++ b/wobuzz/wobuzzm3u/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +def __getattr__(name): + match name: + case "WobuzzM3U": + from .wobuzzm3u import WobuzzM3U + + return WobuzzM3U + + case "WBZM3UData": + from .wbzm3u_data import WBZM3UData + + return WBZM3UData diff --git a/wobuzz/wobuzzm3u/wbzm3u_data.py b/wobuzz/wobuzzm3u/wbzm3u_data.py new file mode 100644 index 0000000..5d1c49c --- /dev/null +++ b/wobuzz/wobuzzm3u/wbzm3u_data.py @@ -0,0 +1,68 @@ +#!/usr/bin/python3 + + +class WBZM3UData: + is_comment = False + type: "WBZM3UData" + + class Header: + is_comment = True + + class Path(str): + pass + + class URL(str): + pass + + class SortOrder: + is_comment = True + + track_title = 0 + track_artist = 1 + track_album = 2 + track_genre = 3 + custom_sorting = 4 + + def __init__(self, sort_by: int, ascending: bool): + self.sort_by = sort_by + self.ascending = ascending + + class TrackMetadata: + class TrackTitle(str): + is_comment = True + + class TrackArtist(str): + is_comment = True + + class TrackAlbum(str): + is_comment = True + + class TrackGenre(str): + is_comment = True + + +class WBZM3UData(WBZM3UData): + class Header(WBZM3UData.Header, WBZM3UData): + pass + + class Path(WBZM3UData.Path, WBZM3UData, str): + pass + + class URL(WBZM3UData.URL, WBZM3UData, str): + pass + + class SortOrder(WBZM3UData.SortOrder, WBZM3UData): + pass + + class TrackMetadata(WBZM3UData.TrackMetadata, WBZM3UData): + class TrackTitle(WBZM3UData.TrackMetadata.TrackTitle, WBZM3UData.TrackMetadata, str): + pass + + class TrackArtist(WBZM3UData.TrackMetadata.TrackArtist, WBZM3UData.TrackMetadata, str): + pass + + class TrackAlbum(WBZM3UData.TrackMetadata.TrackAlbum, WBZM3UData.TrackMetadata, str): + pass + + class TrackGenre(WBZM3UData.TrackMetadata.TrackGenre, str): + pass diff --git a/wobuzz/wobuzzm3u/wobuzzm3u.py b/wobuzz/wobuzzm3u/wobuzzm3u.py new file mode 100644 index 0000000..c536483 --- /dev/null +++ b/wobuzz/wobuzzm3u/wobuzzm3u.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 + +from . import WBZM3UData + + +class WobuzzM3U: + sort_orders = { + "Title": WBZM3UData.SortOrder.track_title, + "Artist": WBZM3UData.SortOrder.track_artist, + "Album": WBZM3UData.SortOrder.track_album, + "Genre": WBZM3UData.SortOrder.track_genre, + "Custom": WBZM3UData.SortOrder.custom_sorting + } + + sort_order_names = { + WBZM3UData.SortOrder.track_title: "Title", + WBZM3UData.SortOrder.track_artist: "Artist", + WBZM3UData.SortOrder.track_album: "Album", + WBZM3UData.SortOrder.track_genre: "Genre", + WBZM3UData.SortOrder.custom_sorting: "Custom" + } + + def __init__(self, filename: str): + self.filename = filename + + def parse_line(self, line: str) -> WBZM3UData | None: + if line.startswith("#"): # comments and EXTM3U/WOBUZZM3U + if line.startswith("#WOBUZZM3U"): + return WBZM3UData.Header() + + elif line.startswith("#SORT: "): # sort + sorting_params = line[6:] # delete "#SORT: " from the line + + sorting = sorting_params.split(", ") # split into the sort column specifier and the sort order + # e.g. ["Title", "Ascending"] + + if not sorting[0] in self.sort_orders: + return None + + sort_by = self.sort_orders[sorting[0]] + order = sorting[1] == "Ascending" + + return WBZM3UData.SortOrder(sort_by, order) + + elif line.startswith("#TRACK_TITLE: "): + return WBZM3UData.TrackMetadata.TrackTitle(line[14:]) + + elif line.startswith("#TRACK_ARTIST: "): + return WBZM3UData.TrackMetadata.TrackArtist(line[15:]) + + elif line.startswith("#TRACK_ALBUM: "): + return WBZM3UData.TrackMetadata.TrackAlbum(line[14:]) + + return None + + elif line.startswith("http"): + return WBZM3UData.URL("URLs currently aren't supported.") + + # line contains a path + return WBZM3UData.Path(line) + + def assemble_line(self, data: WBZM3UData) -> str | None: + if isinstance(data, WBZM3UData.Header): + return "#WOBUZZM3U\n" + + if isinstance(data, WBZM3UData.Path): + return f"{data}\n" + + if isinstance(data, WBZM3UData.URL): + return None + + if isinstance(data, WBZM3UData.SortOrder): + direction = "Ascending" if data.ascending else "Descending" + + return f"#SORT: {self.sort_order_names[data.sort_by]}, {direction}\n" + + if isinstance(data, WBZM3UData.TrackMetadata.TrackTitle): + return f"#TRACK_TITLE: {data}\n" + + if isinstance(data, WBZM3UData.TrackMetadata.TrackArtist): + return f"#TRACK_ARTIST: {data}\n" + + if isinstance(data, WBZM3UData.TrackMetadata.TrackAlbum): + return f"#TRACK_ALBUM: {data}\n" + + if isinstance(data, WBZM3UData.TrackMetadata.TrackGenre): + return f"#TRACK_GENRE: {data}\n" From 209335b00548e45c822d3d51f00b45d711550307 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 7 Mar 2025 19:24:41 +0100 Subject: [PATCH 095/109] Added display of track genres. --- wobuzz/player/playlist.py | 4 ++-- wobuzz/player/track.py | 7 ++++++- wobuzz/ui/playlist_view.py | 7 ++++--- wobuzz/ui/track.py | 9 ++++++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 694ee4c..6df554d 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -284,7 +284,7 @@ class Playlist: def save(self): first_view = list(self.views.values())[0] - first_view.sortItems(4, Qt.SortOrder.AscendingOrder) + first_view.sortItems(5, Qt.SortOrder.AscendingOrder) # sort by custom sorting self.sync(first_view) wbzm3u = WobuzzM3U(self.path) @@ -299,7 +299,7 @@ class Playlist: wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackTitle(track.metadata.title)) wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackArtist(track.metadata.artist)) wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackAlbum(track.metadata.album)) - # wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackGenre(track.metadata.genre)) + wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackGenre(track.metadata.genre)) wbz_data += wbzm3u.assemble_line(WBZM3UData.Path(track.path)) diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index 09d497d..face0dd 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -13,6 +13,7 @@ class TrackMetadata: title: str | None=None artist: str | None=None album: str | None=None + genre: str | None=None images: TTImages | None=None # tinytag images def add_missing(self): @@ -26,15 +27,19 @@ class TrackMetadata: if self.album == "None": self.album = "" + if self.genre == "None": + self.genre = "" + if self.path is None: # can't add missing information without a path return - if self.title is None or self.artist is None or self.album is None: + if self.title is None or self.artist is None or self.album is None or self.genre is None: tags = TinyTag.get(self.path, ignore_errors=True, duration=False) self.title = tags.title self.artist = tags.artist self.album = tags.album + self.genre = tags.genre class Track: diff --git a/wobuzz/ui/playlist_view.py b/wobuzz/ui/playlist_view.py index 6f09edd..b9e3958 100644 --- a/wobuzz/ui/playlist_view.py +++ b/wobuzz/ui/playlist_view.py @@ -30,13 +30,14 @@ class PlaylistView(QTreeWidget): self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setColumnCount(4) + self.setColumnCount(5) headers = [ "#", "Title", "Artist", "Album", + "Genre", "# Custom Sorting" ] @@ -98,7 +99,7 @@ class PlaylistView(QTreeWidget): track.setText(0, str(i)) # 0 = index if user_sort: - track.setText(4, str(i)) # 4 = user sort index + track.setText(5, str(i)) # 5 = user sort index if user_sort: # set last sort to user sort @@ -107,7 +108,7 @@ class PlaylistView(QTreeWidget): self.playlist.sorting.append(WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True)) - self.header.setSortIndicator(4, Qt.SortOrder.AscendingOrder) + self.header.setSortIndicator(5, Qt.SortOrder.AscendingOrder) self.playlist.sync(self, user_sort) # sync playlist to this view diff --git a/wobuzz/ui/track.py b/wobuzz/ui/track.py index f67e21e..aa73ecc 100644 --- a/wobuzz/ui/track.py +++ b/wobuzz/ui/track.py @@ -41,26 +41,29 @@ class TrackItem(QTreeWidgetItem): self.setText(1, track.metadata.title) self.setText(2, track.metadata.artist) self.setText(3, track.metadata.album) - self.setText(4, str(self.index_user_sort + 1)) + self.setText(4, track.metadata.genre) + self.setText(5, str(self.index_user_sort + 1)) def mark(self): self.setIcon(0, self.playing_mark) self.setFont(1, self.bold_font) self.setFont(2, self.bold_font) - self.setFont(3, self.normal_font) + self.setFont(3, self.bold_font) + self.setFont(4, self.bold_font) def unmark(self): self.setIcon(0, QIcon(None)) self.setFont(1, self.normal_font) self.setFont(2, self.normal_font) self.setFont(3, self.normal_font) + self.setFont(4, self.normal_font) def __lt__(self, other): # make numeric strings get sorted the right way column = self.parent.sortColumn() - if column == 0 or column == 4: + if column == 0 or column == 5: return int(self.text(column)) < int(other.text(column)) else: From 259b45335836b22ca427ff8a98f4851b4a0a5f00 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 7 Mar 2025 20:22:39 +0100 Subject: [PATCH 096/109] Removed an unused, by Pycharm autogenerated import. --- wobuzz/player/playlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 6df554d..ede51a1 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -5,7 +5,6 @@ import threading from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QAbstractItemView -from reactivex.observable.case import case_ from .track import Track, TrackMetadata from ..wobuzzm3u import WobuzzM3U, WBZM3UData From 4ae398c6aada3422d89dbe788be74a849cd4311a Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 7 Mar 2025 20:27:18 +0100 Subject: [PATCH 097/109] Made the tracks get copied into the library on import. --- wobuzz/library/library.py | 33 ++++++++++++++++++++++++++++++++- wobuzz/player/playlist.py | 8 +++++++- wobuzz/ui/popups.py | 5 ++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 21ebad0..a59b1f7 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import os +import shutil from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from ..player.playlist import Playlist @@ -114,6 +115,30 @@ class Library: playlist.load() + def import_tracks(self, tracks: list[str]): + playlist = Playlist(self.app, "Temporary Playlist", tracks, True) + + self.replace_temporary_playlist(playlist) + + self.load_playlist_views() + playlist.load() + + def import_track(self, track): + artist_path = os.path.expanduser(f"{self.app.settings.library_path}/artists/{track.metadata.artist}") + + if not os.path.exists(artist_path): + os.makedirs(artist_path) + + new_track_path = f"{artist_path}/{track.path.split("/")[-1]}" + + if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library + return + + shutil.copyfile(track.path, new_track_path) + + track.path = new_track_path + track.metadata.path = new_track_path + def open_playlist(self, playlist_path: str): playlist = Playlist(self.app, "Temporary Playlist", playlist_path) @@ -124,7 +149,13 @@ class Library: playlist.load() def import_playlist(self, playlist_path: str): - self.open_playlist(playlist_path) + playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_tracks=True) + + self.replace_temporary_playlist(playlist) + + self.load_playlist_views() + + playlist.load() def loaded_track(self, track_path: str): """ diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index ede51a1..fdc0580 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -11,7 +11,7 @@ from ..wobuzzm3u import WobuzzM3U, WBZM3UData class Playlist: - def __init__(self, app, title: str, load_from=None): + def __init__(self, app, title: str, load_from=None, import_tracks: bool=False): self.app = app self.title = title # playlist title @@ -20,6 +20,8 @@ class Playlist: # if None, playlist should be already in the library and will be loaded from a .wbz.m3u self.load_from = load_from + self.import_tracks = import_tracks + # add to unique names so if the playlist is loaded from disk, # no other playlist can be created using the same name self.app.utils.unique_names.append(self.title) @@ -94,6 +96,10 @@ class Playlist: self.loading = False + if self.import_tracks: + for track in self.tracks: + self.app.library.import_track(track) + for dock_id in self.views: # enable drag and drop on every view view = self.views[dock_id] diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py index 1b54bb4..2d5b0b2 100644 --- a/wobuzz/ui/popups.py +++ b/wobuzz/ui/popups.py @@ -40,7 +40,10 @@ class Popups: self.app.library.open_tracks(files) def import_tracks(self): - self.open_tracks() # placeholder + files = self.select_audio_files() + + if files is not None and not files == []: + self.app.library.import_tracks(files) def open_playlist(self): playlist_path = self.select_playlist_file() From 31b2e3bf41715699e6bdabcc9c4be82fda9018d6 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 7 Mar 2025 20:31:51 +0100 Subject: [PATCH 098/109] Made the tracks also get copied into the library when loaded from a playlist. --- wobuzz/ui/popups.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py index 2d5b0b2..324e53f 100644 --- a/wobuzz/ui/popups.py +++ b/wobuzz/ui/popups.py @@ -52,4 +52,7 @@ class Popups: self.app.library.open_playlist(playlist_path) def import_playlist(self): - self.open_playlist() # placeholder + playlist_path = self.select_playlist_file() + + if playlist_path is not None and not playlist_path == "": + self.app.library.import_playlist(playlist_path) From fd34476d0087fc7548085e46557d1110f16a6141 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 8 Mar 2025 17:50:45 +0100 Subject: [PATCH 099/109] Added popup on import where the user can configure how the tracks get imported. --- wobuzz/library/__init__.py | 7 ++ wobuzz/library/library.py | 30 ++++--- wobuzz/player/playlist.py | 10 +-- wobuzz/player/track.py | 19 +++++ wobuzz/types/__init__.py | 15 ++++ wobuzz/types/import_options.py | 18 ++++ wobuzz/types/types.py | 9 ++ wobuzz/ui/custom_widgets/__init__.py | 8 ++ wobuzz/ui/custom_widgets/group_box.py | 18 ++++ wobuzz/ui/library/import_dialog.py | 116 ++++++++++++++++++++++++++ wobuzz/ui/popups.py | 56 +++++++++++-- wobuzz/ui/settings/sub_category.py | 11 +-- 12 files changed, 289 insertions(+), 28 deletions(-) create mode 100644 wobuzz/types/__init__.py create mode 100644 wobuzz/types/import_options.py create mode 100644 wobuzz/types/types.py create mode 100644 wobuzz/ui/custom_widgets/__init__.py create mode 100644 wobuzz/ui/custom_widgets/group_box.py create mode 100644 wobuzz/ui/library/import_dialog.py diff --git a/wobuzz/library/__init__.py b/wobuzz/library/__init__.py index a93a4bf..8fc6dc1 100644 --- a/wobuzz/library/__init__.py +++ b/wobuzz/library/__init__.py @@ -1 +1,8 @@ #!/usr/bin/python3 + +def __getattr__(name): + match name: + case "Library": + from .library import Library + + return Library diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index a59b1f7..56f88f8 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -1,12 +1,12 @@ #!/usr/bin/python3 import os -import shutil from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from ..player.playlist import Playlist from ..ui.library.library import LibraryWidget from ..ui.playlist_view import PlaylistView +from ..types import Types class Library: @@ -115,15 +115,28 @@ class Library: playlist.load() - def import_tracks(self, tracks: list[str]): - playlist = Playlist(self.app, "Temporary Playlist", tracks, True) + def import_tracks( + self, + tracks: list[str], + import_options: Types.ImportOptions + ): + playlist = Playlist(self.app, "Temporary Playlist", tracks, import_options) self.replace_temporary_playlist(playlist) self.load_playlist_views() playlist.load() - def import_track(self, track): + def import_track(self, track, import_options: Types.ImportOptions): + if import_options.artist is not None: + track.metadata.artist = import_options.artist + + if import_options.album is not None: + track.metadata.album = import_options.album + + if import_options.genre is not None: + track.metadata.genre = import_options.genre + artist_path = os.path.expanduser(f"{self.app.settings.library_path}/artists/{track.metadata.artist}") if not os.path.exists(artist_path): @@ -134,10 +147,7 @@ class Library: if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library return - shutil.copyfile(track.path, new_track_path) - - track.path = new_track_path - track.metadata.path = new_track_path + track.copy(new_track_path, import_options.copy_type) def open_playlist(self, playlist_path: str): playlist = Playlist(self.app, "Temporary Playlist", playlist_path) @@ -148,8 +158,8 @@ class Library: playlist.load() - def import_playlist(self, playlist_path: str): - playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_tracks=True) + def import_playlist(self, playlist_path: str, import_options): + playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_options) self.replace_temporary_playlist(playlist) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index fdc0580..0d5fd2e 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -2,16 +2,16 @@ import os import threading - from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QAbstractItemView from .track import Track, TrackMetadata from ..wobuzzm3u import WobuzzM3U, WBZM3UData +from ..types import Types class Playlist: - def __init__(self, app, title: str, load_from=None, import_tracks: bool=False): + def __init__(self, app, title: str, load_from=None, import_options: Types.ImportOptions=None): self.app = app self.title = title # playlist title @@ -20,7 +20,7 @@ class Playlist: # if None, playlist should be already in the library and will be loaded from a .wbz.m3u self.load_from = load_from - self.import_tracks = import_tracks + self.import_options = import_options # add to unique names so if the playlist is loaded from disk, # no other playlist can be created using the same name @@ -96,9 +96,9 @@ class Playlist: self.loading = False - if self.import_tracks: + if self.import_options is not None: for track in self.tracks: - self.app.library.import_track(track) + self.app.library.import_track(track, self.import_options) for dock_id in self.views: # enable drag and drop on every view view = self.views[dock_id] diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index face0dd..faf0f8b 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -1,11 +1,15 @@ #!/usr/bin/python3 +import os +import shutil from pydub import AudioSegment from pygame.mixer import Sound from tinytag import TinyTag from tinytag.tinytag import Images as TTImages from dataclasses import dataclass +from ..types import Types + @dataclass class TrackMetadata: @@ -163,3 +167,18 @@ class Track: self.items.remove(item) self.occurrences.pop(playlist) + + def copy(self, dest: str, copy_type: int=Types.CopyType.symlink, moved: bool=True): + match copy_type: + case Types.CopyType.symlink: + os.symlink(self.path, dest) + + case Types.CopyType.copy: + shutil.copyfile(self.path, dest) + + case Types.CopyType.move: + shutil.move(self.path, dest) + + if moved: # update path variables + self.path = dest + self.metadata.path = dest diff --git a/wobuzz/types/__init__.py b/wobuzz/types/__init__.py new file mode 100644 index 0000000..052adfa --- /dev/null +++ b/wobuzz/types/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/python3 + +def __getattr__(name): + match name: + case "Types": + from .types import Types + return Types + + case "ImportOptions": + from .import_options import ImportOptions + return ImportOptions + + case "CopyType": + from .import_options import CopyType + return CopyType diff --git a/wobuzz/types/import_options.py b/wobuzz/types/import_options.py new file mode 100644 index 0000000..ceed274 --- /dev/null +++ b/wobuzz/types/import_options.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +from dataclasses import dataclass + + +@dataclass +class ImportOptions: + artist: str=None + album: str=None + genre: str=None + + copy_type=0 + + +class CopyType: + symlink = 0 + copy = 1 + move = 2 diff --git a/wobuzz/types/types.py b/wobuzz/types/types.py new file mode 100644 index 0000000..dbbab54 --- /dev/null +++ b/wobuzz/types/types.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +from . import ImportOptions +from . import CopyType + + +class Types: + ImportOptions = ImportOptions + CopyType = CopyType diff --git a/wobuzz/ui/custom_widgets/__init__.py b/wobuzz/ui/custom_widgets/__init__.py new file mode 100644 index 0000000..6cfdb5a --- /dev/null +++ b/wobuzz/ui/custom_widgets/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python3 + + +def __getattr__(name): + match name: + case "GroupBox": + from .group_box import GroupBox + return GroupBox diff --git a/wobuzz/ui/custom_widgets/group_box.py b/wobuzz/ui/custom_widgets/group_box.py new file mode 100644 index 0000000..8b61620 --- /dev/null +++ b/wobuzz/ui/custom_widgets/group_box.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QGroupBox, QSizePolicy + + +class GroupBox(QGroupBox): + """ + Just a QGroupBox with some custom style I don't always want to rewrite. + """ + + def __init__(self, title, parent=None): + super().__init__(title, parent) + + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) + + self.setStyleSheet("QGroupBox{font-weight: bold;}") diff --git a/wobuzz/ui/library/import_dialog.py b/wobuzz/ui/library/import_dialog.py new file mode 100644 index 0000000..e37bfbe --- /dev/null +++ b/wobuzz/ui/library/import_dialog.py @@ -0,0 +1,116 @@ +#!/usr/bin/python3 + +from PyQt6.QtWidgets import ( + QWidget, + QLabel, + QDialog, + QCheckBox, + QLineEdit, + QDialogButtonBox, + QButtonGroup, + QRadioButton, + QVBoxLayout, + QHBoxLayout, + QFormLayout, + QSizePolicy, +) +from PyQt6.QtCore import Qt + +from ..custom_widgets import GroupBox + + +class ImportDialog(QDialog): + def __init__(self): + super().__init__() + + self.setWindowTitle("Import") + + layout = QVBoxLayout(self) + self.setLayout(layout) + + self.tagging_section = GroupBox("Metadata And Tagging", self) + self.tagging_section.layout = QFormLayout(self.tagging_section) + self.tagging_section.setLayout(self.tagging_section.layout) + layout.addWidget(self.tagging_section) + + self.tagging_section.overwrite_metadata = QCheckBox( + "Set custom metadata for all tracks (Leave property blank to keep the metadata from the audio file.)", + self.tagging_section + ) + self.tagging_section.layout.addRow(self.tagging_section.overwrite_metadata) + + self.tagging_section.artist = QLineEdit(self.tagging_section) + self.tagging_section.layout.addRow(" Artist: ", self.tagging_section.artist) + self.tagging_section.artist.setPlaceholderText("Keep track artist") + + self.tagging_section.album = QLineEdit(self.tagging_section) + self.tagging_section.layout.addRow(" Album: ", self.tagging_section.album) + self.tagging_section.album.setPlaceholderText("Keep track album") + + self.tagging_section.genre = QLineEdit(self.tagging_section) + self.tagging_section.layout.addRow(" Genre: ", self.tagging_section.genre) + self.tagging_section.genre.setPlaceholderText("Keep track genre") + + self.file_section = GroupBox("File Structure", self) + self.file_section.layout = QFormLayout(self.file_section) + self.file_section.setLayout(self.file_section.layout) + layout.addWidget(self.file_section) + + self.file_section.copy_type_description = QLabel("How should the tracks get put into the Wobuzz library?") + self.file_section.layout.addRow(self.file_section.copy_type_description) + + self.file_section.copy_type = QButtonGroup(self.file_section) + + self.file_section.copy_type_symlink = QRadioButton("Create symlinks", self.file_section) + self.file_section.copy_type_symlink.setChecked(True) + self.file_section.copy_type.addButton(self.file_section.copy_type_symlink) + self.file_section.layout.addWidget(self.file_section.copy_type_symlink) + + self.file_section.copy_type_copy = QRadioButton("Copy tracks", self.file_section) + self.file_section.copy_type.addButton(self.file_section.copy_type_copy) + self.file_section.layout.addWidget(self.file_section.copy_type_copy) + + self.file_section.copy_type_move = QRadioButton("Move tracks", self.file_section) + self.file_section.copy_type.addButton(self.file_section.copy_type_move) + self.file_section.layout.addWidget(self.file_section.copy_type_move) + + # add expanding widget so the GroupBoxes aren't vertically centered + spacer_widget = QWidget(self) + spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + layout.addWidget(spacer_widget) + + dialog_buttons = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + + self.dialog_buttons = QDialogButtonBox(dialog_buttons) + layout.addWidget(self.dialog_buttons) + + self.reset_inputs() + + self.tagging_section.overwrite_metadata.stateChanged.connect(self.overwrite_state_changed) + self.dialog_buttons.accepted.connect(self.accept) + self.dialog_buttons.rejected.connect(self.reject) + + def exec(self): + self.reset_inputs() + + return super().exec() + + def overwrite_state_changed(self, state: int): + overwrite = state == 2 + + self.tagging_section.artist.setEnabled(overwrite) + self.tagging_section.album.setEnabled(overwrite) + self.tagging_section.genre.setEnabled(overwrite) + + self.tagging_section.layout.setRowVisible(self.tagging_section.artist, overwrite) + self.tagging_section.layout.setRowVisible(self.tagging_section.album, overwrite) + self.tagging_section.layout.setRowVisible(self.tagging_section.genre, overwrite) + + def reset_inputs(self): + self.overwrite_state_changed(0) + + self.tagging_section.artist.setText("") + self.tagging_section.album.setText("") + self.tagging_section.genre.setText("") + + self.file_section.copy_type_symlink.setChecked(True) diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py index 324e53f..7b7ec13 100644 --- a/wobuzz/ui/popups.py +++ b/wobuzz/ui/popups.py @@ -1,6 +1,9 @@ #!/usr/bin/python3 -from PyQt6.QtWidgets import QFileDialog +from PyQt6.QtWidgets import QDialog, QFileDialog + +from .library.import_dialog import ImportDialog +from ..types import Types class Popups: @@ -12,7 +15,7 @@ class Popups: self.audio_file_selector = QFileDialog(self.window, "Select Audio Files") self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) - self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus)", "Any (*)"]) + self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus *.m4a)", "Any (*)"]) self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List) self.playlist_file_selector = QFileDialog(self.window, "Select Playlist") @@ -20,6 +23,8 @@ class Popups: self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"]) self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) + self.import_dialog = ImportDialog() + self.window.open_track_action.triggered.connect(self.open_tracks) self.window.import_track_action.triggered.connect(self.import_tracks) self.window.open_playlist_action.triggered.connect(self.open_playlist) @@ -39,11 +44,43 @@ class Popups: if files is not None and not files == []: self.app.library.open_tracks(files) + def get_import_options(self): + import_options = Types.ImportOptions() + + if self.import_dialog.tagging_section.overwrite_metadata.isChecked(): + artist = self.import_dialog.tagging_section.artist.text() + album = self.import_dialog.tagging_section.album.text() + genre = self.import_dialog.tagging_section.genre.text() + + if not artist == "": + import_options.artist = artist + + if not album == "": + import_options.album = album + + if not genre == "": + import_options.genre = genre + + if self.import_dialog.file_section.copy_type_copy.isChecked(): + import_options.copy_type = Types.CopyType.copy + + elif self.import_dialog.file_section.copy_type_move.isChecked(): + import_options.copy_type = Types.CopyType.move + + return import_options + def import_tracks(self): files = self.select_audio_files() - if files is not None and not files == []: - self.app.library.import_tracks(files) + if files is None or files == []: + return + + if self.import_dialog.exec() == QDialog.rejected: + return + + import_options = self.get_import_options() + + self.app.library.import_tracks(files, import_options) def open_playlist(self): playlist_path = self.select_playlist_file() @@ -54,5 +91,12 @@ class Popups: def import_playlist(self): playlist_path = self.select_playlist_file() - if playlist_path is not None and not playlist_path == "": - self.app.library.import_playlist(playlist_path) + if playlist_path is None or playlist_path == "": + return + + if self.import_dialog.exec() == QDialog.rejected: + return + + import_options = self.get_import_options() + + self.app.library.import_playlist(playlist_path, import_options) diff --git a/wobuzz/ui/settings/sub_category.py b/wobuzz/ui/settings/sub_category.py index 694b682..6453b78 100644 --- a/wobuzz/ui/settings/sub_category.py +++ b/wobuzz/ui/settings/sub_category.py @@ -2,21 +2,18 @@ from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont -from PyQt6.QtWidgets import QGroupBox, QLabel, QSizePolicy, QFormLayout +from PyQt6.QtWidgets import QLabel, QSizePolicy, QFormLayout + +from ..custom_widgets import GroupBox -class SubCategory(QGroupBox): +class SubCategory(GroupBox): description_font = QFont() description_font.setPointSize(8) def __init__(self, title: str, description: str=None, parent=None): super().__init__(title, parent) - self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) - self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) - - self.setStyleSheet("QGroupBox{font-weight: bold;}") - self.layout = QFormLayout() self.setLayout(self.layout) From 36b085d38ad0f37bd8eed43e7f6ee32dd966dd2b Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 8 Mar 2025 18:08:46 +0100 Subject: [PATCH 100/109] Added a screenshot. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 73622af..7e353d7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Wobuzz is a simple audio player made by The Wobbler. Currently, it just has really basic features but many more things are planned. The player has its own playlist file format that is similar to extended m3u. [WOBUZZM3U](https://gulm.i21k.de/index.php?title=WOBUZZM3U) +![](https://emil.i21k.de/files/Wobuzz-Screenshot.png) + Please note that [the repository on teapot.informationsanarchistik.de](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz) is the original repository and the [repository on Codeberg](https://codeberg.org/Wobbl/Wobuzz) is a mirror, normal users only have read rights on the original repository because registration is disabled on the server. Issues and pull-requests are not synced. From e0c4843f06f74305f328fbde2d8f989f308b5ca3 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 13 Mar 2025 14:20:12 +0100 Subject: [PATCH 101/109] Added some small shit that will get used in the future and made the code compatible with Python>=3.10. (According to Pycharm) --- wobuzz/library/library.py | 7 ++++++- wobuzz/player/playlist.py | 2 +- wobuzz/ui/playlist_tabs/tab_context_menu.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index 56f88f8..a9c8c14 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -128,21 +128,26 @@ class Library: playlist.load() def import_track(self, track, import_options: Types.ImportOptions): + change_metadata = False + if import_options.artist is not None: track.metadata.artist = import_options.artist + change_metadata = True if import_options.album is not None: track.metadata.album = import_options.album + change_metadata = True if import_options.genre is not None: track.metadata.genre = import_options.genre + change_metadata = True artist_path = os.path.expanduser(f"{self.app.settings.library_path}/artists/{track.metadata.artist}") if not os.path.exists(artist_path): os.makedirs(artist_path) - new_track_path = f"{artist_path}/{track.path.split("/")[-1]}" + new_track_path = f"{artist_path}/{track.path.split('/')[-1]}" if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library return diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 0d5fd2e..1fc7d2b 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -365,7 +365,7 @@ class Playlist: def path_from_title(self, title): path = os.path.expanduser( - f"{self.app.settings.library_path}/playlists/{title.replace(" ", "_")}.wbz.m3u" + f"{self.app.settings.library_path}/playlists/{title.replace(' ', '_')}.wbz.m3u" ) return path diff --git a/wobuzz/ui/playlist_tabs/tab_context_menu.py b/wobuzz/ui/playlist_tabs/tab_context_menu.py index 6d78b80..b235f9d 100644 --- a/wobuzz/ui/playlist_tabs/tab_context_menu.py +++ b/wobuzz/ui/playlist_tabs/tab_context_menu.py @@ -27,6 +27,7 @@ class PlaylistContextMenu(QMenu): self.rename_action.triggered.connect(self.rename) self.delete_action.triggered.connect(self.delete) + # noinspection PyMethodOverriding def exec(self, pos: QPoint, index: int, playlist): self.tab_index = index self.playlist = playlist From b0a81d7176a84722bae1091735f1a3b2424284e9 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 13 Mar 2025 14:33:27 +0100 Subject: [PATCH 102/109] README.md: Added compatibility description. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 7e353d7..06cf37b 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,14 @@ In the future, this may get optimized and CPU-usage could increase due to more f ## Installation +#### Compatibility + +Wobuzz was only made for Linux. +It should not require that much effort to make it compatible with windows or mac, but why should anyone do that??? +Currently (v0.1a2) Wobuzz is not really tested for compatibility, but it should run without problems on rather +new Debian-based distros like Mint 22. \ +Also, the Python version should be newer than Python3.9 + ### Release installation Look at the [Releases](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz/releases), From 3f6b40e5fe6418b6ab34d7b1e9fe29dd261c0754 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 13 Mar 2025 14:49:19 +0100 Subject: [PATCH 103/109] Disabled the "Overwrite Metadata"-checkbox because writing tags is not yet implemented. --- wobuzz/ui/library/import_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wobuzz/ui/library/import_dialog.py b/wobuzz/ui/library/import_dialog.py index e37bfbe..5612515 100644 --- a/wobuzz/ui/library/import_dialog.py +++ b/wobuzz/ui/library/import_dialog.py @@ -38,6 +38,7 @@ class ImportDialog(QDialog): self.tagging_section ) self.tagging_section.layout.addRow(self.tagging_section.overwrite_metadata) + self.tagging_section.overwrite_metadata.setEnabled(False) # writing of metadata not yet implemented self.tagging_section.artist = QLineEdit(self.tagging_section) self.tagging_section.layout.addRow(" Artist: ", self.tagging_section.artist) From 4cd6482571dadfdc818a48e39cbcd17e738472c9 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 6 Apr 2025 18:45:20 +0200 Subject: [PATCH 104/109] Hardcoded some shortcuts. --- wobuzz/ui/main_window.py | 7 +++++-- wobuzz/ui/track_control.py | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index f3036bb..35c9e6b 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt, QPoint -from PyQt6.QtGui import QIcon, QContextMenuEvent +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIcon, QShortcut from PyQt6.QtWidgets import QMainWindow, QMenu from .track_control import TrackControl from .settings import Settings @@ -58,3 +58,6 @@ class MainWindow(QMainWindow): dock_menu = self.createPopupMenu() dock_menu.setTitle("Docks And Toolbars") self.view_menu.addMenu(dock_menu) + + close_shortcut = QShortcut("Ctrl+Q", self) + close_shortcut.activated.connect(self.close) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 2aa1068..770e6f8 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -18,14 +18,18 @@ class TrackControl(QToolBar): icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipBackward) self.previous_button = self.addAction(icon, "Previous") + self.previous_button.setShortcut("Shift+Left") self.toggle_play_button = self.addAction(self.play_icon, "Play/Pause") + self.toggle_play_button.setShortcut("Space") icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStop) self.stop_button = self.addAction(icon, "Stop") + self.stop_button.setShortcut("Shift+S") icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipForward) self.next_button = self.addAction(icon, "Next") + self.next_button.setShortcut("Shift+Right") self.progress_indicator = QLabel("0:00") self.addWidget(self.progress_indicator) From e845c41ca37e7e46180d72b304734b84a100ef46 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Thu, 10 Apr 2025 12:00:11 +0200 Subject: [PATCH 105/109] Made the Playlist.save()-function write a header again. --- wobuzz/player/playlist.py | 2 ++ wobuzz/wobuzzm3u/wobuzzm3u.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index 1fc7d2b..0bba6a7 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -296,6 +296,8 @@ class Playlist: wbz_data = "" + wbz_data += wbzm3u.assemble_line(WBZM3UData.Header) + for order in self.sorting: wbz_data += wbzm3u.assemble_line(order) diff --git a/wobuzz/wobuzzm3u/wobuzzm3u.py b/wobuzz/wobuzzm3u/wobuzzm3u.py index c536483..b9891f4 100644 --- a/wobuzz/wobuzzm3u/wobuzzm3u.py +++ b/wobuzz/wobuzzm3u/wobuzzm3u.py @@ -60,7 +60,7 @@ class WobuzzM3U: return WBZM3UData.Path(line) def assemble_line(self, data: WBZM3UData) -> str | None: - if isinstance(data, WBZM3UData.Header): + if data is WBZM3UData.Header or isinstance(data, WBZM3UData.Header): return "#WOBUZZM3U\n" if isinstance(data, WBZM3UData.Path): From 9416ac6737d83bf2d663255f4409666f3862bfe5 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sat, 12 Apr 2025 22:00:29 +0200 Subject: [PATCH 106/109] Implemented basic MPRIS integration. --- README.md | 19 ++++---- requirements.txt | 12 +++-- setup.py | 13 ++--- wobuzz/player/mpris.py | 98 ++++++++++++++++++++++++++++++++++++++ wobuzz/player/player.py | 26 ++++++++++ wobuzz/ui/track_control.py | 32 +++++-------- 6 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 wobuzz/player/mpris.py diff --git a/README.md b/README.md index 06cf37b..789a27e 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,17 @@ Please note that [the repository on teapot.informationsanarchistik.de](https://t normal users only have read rights on the original repository because registration is disabled on the server. Issues and pull-requests are not synced. -### Features +### Features (Implemented & Planned) -| Feature | Description | State | -|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented | -| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented | -| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented | -| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | Not Implemented | -| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | Not Implemented | -| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | Not Implemented | +| Feature | Description | State | +|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| +| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented | +| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented | +| MPRIS Integration | Basic MPRIS integration (Partial Metadata, Play, Pause, Stop, Next, Previous) | Partially Implemented | +| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented | +| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | Not Implemented | +| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | Not Implemented | +| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | Not Implemented | ### Performance diff --git a/requirements.txt b/requirements.txt index f4f580b..3fbe474 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -PyQt6 -pygame -tinytag -pydub -wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools \ No newline at end of file +PyQt6~=6.8.0 +pygame~=2.6.1 +tinytag~=2.1.0 +pydub~=0.25.1 +wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools +sdbus~=0.14.0 +setuptools~=68.1.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 790fec4..5df990e 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description = (this_directory / "README.md").read_text() setuptools.setup( name="Wobuzz", - version="0.1a2", + version="0.1a3", description="An audio player made by The Wobbler", long_description=long_description, long_description_content_type="text/markdown", @@ -30,11 +30,12 @@ setuptools.setup( packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]), package_data={"": ["*.txt", "*.svg"]}, install_requires=[ - "PyQt6", - "tinytag", - "pydub", - "pygame", - "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools" + "PyQt6~=6.8.0", + "tinytag~=2.1.0", + "pydub~=0.25.1", + "pygame~=2.6.1", + "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools", + "sdbus~=0.14.0" ], entry_points={ "console_scripts": ["wobuzz=wobuzz.command_line:main"], diff --git a/wobuzz/player/mpris.py b/wobuzz/player/mpris.py new file mode 100644 index 0000000..f4c3146 --- /dev/null +++ b/wobuzz/player/mpris.py @@ -0,0 +1,98 @@ +from sdbus import ( + dbus_method_async, + dbus_property_async, + DbusInterfaceCommonAsync, + request_default_bus_name_async +) +import asyncio + +from wobuzz.player.track import TrackMetadata + +SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" +OBJECT_PATH = "/org/mpris/MediaPlayer2" +MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" +MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" + + +class MPRISRoot(DbusInterfaceCommonAsync, interface_name=MPRIS_ROOT_INTERFACE): + @dbus_method_async() + async def Raise(self): + print("Raise, maybe?") + + @dbus_property_async("s") + def Identity(self): + return "Wobuzz" + + +class MPRISPlayer(DbusInterfaceCommonAsync, interface_name=MPRIS_PLAYER_INTERFACE): + def __init__(self): + super().__init__() + + self._metadata = {} + + @dbus_property_async("b") + def CanSeek(self): + return False + + @dbus_method_async() + async def PlayPause(self): + self.app.gui.track_control.toggle_playing_signal.emit() + + @dbus_method_async() + async def Next(self): + self.app.gui.track_control.next_signal.emit() + + @dbus_method_async() + async def Previous(self): + self.app.gui.track_control.previous_signal.emit() + + @dbus_method_async() + async def Stop(self): + self.app.gui.track_control.stop_signal.emit() + + @dbus_property_async("a{sv}") + def Metadata(self): + return self._metadata + + @Metadata.setter # noqa + def Metadata_setter(self, metadata: dict) -> None: + self._metadata = metadata + + async def set_metadata(self, metadata: TrackMetadata): + await self.Metadata.set_async(self.to_xesam(metadata)) + + def to_xesam(self, metadata: "TrackMetadata") -> dict: + xesam_metadata = { + "mpris:trackid": ("s", "kjuztuktg"), + "xesam:title": ("s", metadata.title), + "xesam:artist": ("as", [metadata.artist]) + } + + return xesam_metadata + + +class MPRISServer(MPRISRoot, MPRISPlayer): + def __init__(self, app): + super().__init__() + + self.app = app + + self.loop = None + + async def setup_bus(self) -> None: + await request_default_bus_name_async(SERVICE_NAME) + self.export_to_dbus(OBJECT_PATH) + + def start(self): + self.loop = asyncio.new_event_loop() + self.loop.run_until_complete(self.setup_bus()) + self.loop.run_forever() + + def exec_async(self, function): + """ + This stupid function somehow allows us to execute an async function from the main thread. + If someone ha a better solution, please improve this. I have no idea of how asyncio works. + """ + + loop = asyncio.new_event_loop() + loop.run_until_complete(function) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index f32d25a..b9bc4e9 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -4,8 +4,10 @@ import time import threading import pygame.mixer import pygame.event + from .playlist import Playlist from .track_progress_timer import TrackProgress +from .mpris import MPRISServer class Player: @@ -21,6 +23,10 @@ class Player: self.history = Playlist(self.app, "History") self.current_playlist = None + self.mpris_server = MPRISServer(self.app) + # start mpris server in a thread (daemon = exit with main thread) + threading.Thread(target=self.mpris_server.start, daemon=True).start() + self.playing = False self.paused = False @@ -34,6 +40,7 @@ class Player: self.paused = False self.app.gui.on_playstate_update() + self.mpris_server.exec_async(self.mpris_server.set_metadata(self.current_playlist.current_track.metadata)) # cache next track so it immediately starts when the current track finishes self.cache_next_track() @@ -191,3 +198,22 @@ class Player: self.current_playlist = playlist self.start_playing() + + def toggle_playing(self): + if self.playing and self.paused: # paused + self.unpause() + + elif self.playing: # playing + self.pause() + + # stopped but tracks in the current playlist + elif self.current_playlist is not None and self.current_playlist.has_tracks(): + self.start_playing() + + elif self.current_playlist is None: + if self.app.settings.latest_playlist is not None: + for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist + if playlist.path == self.app.settings.latest_playlist: + self.start_playlist(playlist) + + break diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 770e6f8..6b41796 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -1,11 +1,18 @@ #!/usr/bin/python3 +from PyQt6.QtCore import pyqtSignal from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QToolBar, QLabel + from .track_progress_slider import TrackProgressSlider class TrackControl(QToolBar): + toggle_playing_signal = pyqtSignal() # signals for MPRIS + next_signal = pyqtSignal() + previous_signal = pyqtSignal() + stop_signal = pyqtSignal() + def __init__(self, app, parent=None): super().__init__(parent) @@ -45,9 +52,13 @@ class TrackControl(QToolBar): def connect(self): self.previous_button.triggered.connect(self.previous_track) - self.toggle_play_button.triggered.connect(self.toggle_playing) + self.previous_signal.connect(self.previous_track) + self.toggle_play_button.triggered.connect(self.app.player.toggle_playing) + self.toggle_playing_signal.connect(self.app.player.toggle_playing) self.stop_button.triggered.connect(self.app.player.stop) + self.stop_signal.connect(self.app.player.stop) self.next_button.triggered.connect(self.next_track) + self.next_signal.connect(self.next_track) def previous_track(self): if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): @@ -73,25 +84,6 @@ class TrackControl(QToolBar): self.track_progress_slider.update_progress() - def toggle_playing(self): - if self.app.player.playing and self.app.player.paused: # paused - self.app.player.unpause() - - elif self.app.player.playing: # playing - self.app.player.pause() - - # stopped but tracks in the current playlist - elif self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): - self.app.player.start_playing() - - elif self.app.player.current_playlist is None: - if self.app.settings.latest_playlist is not None: - for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist - if playlist.path == self.app.settings.latest_playlist: - self.app.player.start_playlist(playlist) - - break - def on_playstate_update(self): if self.app.player.playing: if self.app.player.paused: From a236370d47790fbb4ffb2829ea501bade250ca5c Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Sun, 13 Apr 2025 16:24:34 +0200 Subject: [PATCH 107/109] Implemented MPRIS metadata "mpris:artUrl" --- wobuzz/player/mpris.py | 6 +++++- wobuzz/player/player.py | 23 +++++++++++++++++++++++ wobuzz/utils.py | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/wobuzz/player/mpris.py b/wobuzz/player/mpris.py index f4c3146..fdf7bf4 100644 --- a/wobuzz/player/mpris.py +++ b/wobuzz/player/mpris.py @@ -62,8 +62,12 @@ class MPRISPlayer(DbusInterfaceCommonAsync, interface_name=MPRIS_PLAYER_INTERFAC await self.Metadata.set_async(self.to_xesam(metadata)) def to_xesam(self, metadata: "TrackMetadata") -> dict: + # cache name by filename without extension + art_path = self.app.utils.tmp_path + "/cover_cache/" + metadata.path.split("/")[-1][:-4] + xesam_metadata = { - "mpris:trackid": ("s", "kjuztuktg"), + "mpris:trackid": ("s", "kjuztuktg"), # nonsense, no functionality + "mpris:artUrl": ("s", "file://" + art_path), "xesam:title": ("s", metadata.title), "xesam:artist": ("as", [metadata.artist]) } diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index b9bc4e9..6946667 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +import os import time import threading import pygame.mixer @@ -40,6 +41,8 @@ class Player: self.paused = False self.app.gui.on_playstate_update() + + self.export_cover_art_tmp() self.mpris_server.exec_async(self.mpris_server.set_metadata(self.current_playlist.current_track.metadata)) # cache next track so it immediately starts when the current track finishes @@ -217,3 +220,23 @@ class Player: self.start_playlist(playlist) break + + def export_cover_art_tmp(self) -> None: + """ + Export the cover art of the current track to /tmp/wobuzz/current_cover, so MPRIS can access it. + """ + + metadata = self.current_playlist.current_track.metadata + + art_tmp_path = self.app.utils.tmp_path + "/cover_cache/" + art_path = art_tmp_path + metadata.path.split("/")[-1][:-4] + + if os.path.isfile(art_path): # cover art already exported + return + + if not os.path.exists(art_tmp_path): + os.makedirs(art_tmp_path) + + file = open(art_path, "wb") + file.write(metadata.images.any.data) + file.close() diff --git a/wobuzz/utils.py b/wobuzz/utils.py index 901b57b..eabba71 100644 --- a/wobuzz/utils.py +++ b/wobuzz/utils.py @@ -8,6 +8,7 @@ class Utils: home_path = str(Path.home()) wobuzz_location = os.path.dirname(os.path.abspath(__file__)) settings_location = f"{wobuzz_location}/settings.json" + tmp_path = "/tmp/wobuzz" def __init__(self, app): self.app = app From f23530628cd2caeebbef5db650c0555073e29277 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 16 Apr 2025 16:57:01 +0200 Subject: [PATCH 108/109] MPRIS: Switching from python-sdbus to jeepney, no functionality. --- requirements.txt | 5 +- wobuzz/gui.py | 4 +- wobuzz/main.py | 4 ++ wobuzz/mpris/__init__.py | 4 ++ wobuzz/mpris/dbus_properties.py | 67 +++++++++++++++++++ wobuzz/mpris/mpris_player.py | 34 ++++++++++ wobuzz/mpris/mpris_root.py | 16 +++++ wobuzz/mpris/server.py | 68 +++++++++++++++++++ wobuzz/mpris/utils.py | 96 +++++++++++++++++++++++++++ wobuzz/player/mpris.py | 102 ----------------------------- wobuzz/player/player.py | 6 -- wobuzz/ui/main_window.py | 6 +- wobuzz/ui/track_control.py | 10 --- wobuzz/ui/track_progress_slider.py | 4 +- 14 files changed, 301 insertions(+), 125 deletions(-) create mode 100644 wobuzz/mpris/__init__.py create mode 100644 wobuzz/mpris/dbus_properties.py create mode 100644 wobuzz/mpris/mpris_player.py create mode 100644 wobuzz/mpris/mpris_root.py create mode 100644 wobuzz/mpris/server.py create mode 100644 wobuzz/mpris/utils.py delete mode 100644 wobuzz/player/mpris.py diff --git a/requirements.txt b/requirements.txt index 3fbe474..7981831 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ pygame~=2.6.1 tinytag~=2.1.0 pydub~=0.25.1 wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools -sdbus~=0.14.0 -setuptools~=68.1.2 \ No newline at end of file +setuptools~=78.1.0 +Wobuzz~=0.1a3 +jeepney~=0.8.0 \ No newline at end of file diff --git a/wobuzz/gui.py b/wobuzz/gui.py index c23dc49..a56082a 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt, QTimer -from PyQt6.QtWidgets import QDockWidget, QFileDialog +from PyQt6.QtCore import QTimer from .ui.main_window import MainWindow from .ui.popups import Popups @@ -76,6 +75,7 @@ class GUI: def on_playstate_update(self): self.track_control.on_playstate_update() self.track_info.update_info() + self.app.mpris_server.on_playstate_update() def update_gui(self): self.track_control.track_progress_slider.update_progress() diff --git a/wobuzz/main.py b/wobuzz/main.py index 6a089bc..14331a8 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -8,6 +8,7 @@ from .utils import Utils from .player import Player from .library.library import Library from .gui import GUI +from .mpris import MPRISServer class Wobuzz: @@ -22,6 +23,9 @@ class Wobuzz: self.library = Library(self) self.player = Player(self) self.gui = GUI(self) + self.mpris_server = MPRISServer(self) + self.gui.window.mpris_signal.connect(self.mpris_server.handle_event) + self.mpris_server.start() self.late_init() diff --git a/wobuzz/mpris/__init__.py b/wobuzz/mpris/__init__.py new file mode 100644 index 0000000..a5a4d67 --- /dev/null +++ b/wobuzz/mpris/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 + +from .server import MPRISServer + diff --git a/wobuzz/mpris/dbus_properties.py b/wobuzz/mpris/dbus_properties.py new file mode 100644 index 0000000..51665f0 --- /dev/null +++ b/wobuzz/mpris/dbus_properties.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 + +from jeepney import Header, MessageType, Endianness, MessageFlag, new_method_return +from jeepney.bus_messages import message_bus +from jeepney.io.blocking import open_dbus_connection +from jeepney.wrappers import new_header + +from .utils import * + + +class DBusProperties(DBusInterface): + def get_all(self): + body = ({},) + return body + + def properties_changed(self, interface: str): + body = None + + if interface == MPRIS_ROOT_INTERFACE: + body = (MPRIS_ROOT_INTERFACE,) + self.server.root_interface.get_all() + + elif interface == MPRIS_PLAYER_INTERFACE: + body = (MPRIS_PLAYER_INTERFACE,) + self.server.player_interface.get_all() + + signature = "" if body is None else "sa{sv}" + + msg = new_signal( + self.server.bus_address.with_interface("org.freedesktop.DBus"), + "PropertiesChanged", + signature, + body + ) + self.server.bus.send(msg) + + def Get(self, msg: Message): + interface_name = msg.body[0] + + return_msg = None + + if interface_name == PROPERTIES_INTERFACE: + return self.get(msg) + + elif interface_name == MPRIS_ROOT_INTERFACE: + return self.server.root_interface.get(msg) + + elif interface_name == MPRIS_PLAYER_INTERFACE: + return self.server.player_interface.get(msg) + + else: + return new_error(msg, *DBusErrors.invalidArgs(interface=interface_name)) + + def GetAll(self, msg: Message): + interface = msg.body[0] + + if interface == PROPERTIES_INTERFACE: + body = self.get_all() + + elif interface == MPRIS_ROOT_INTERFACE: + body = self.server.root_interface.get_all() + + elif interface == MPRIS_PLAYER_INTERFACE: + body = self.server.player_interface.get_all() + + else: + return new_error(msg, *DBusErrors.invalidArgs(interface=interface)) + + return new_method_return(msg, "a{sv}", body) diff --git a/wobuzz/mpris/mpris_player.py b/wobuzz/mpris/mpris_player.py new file mode 100644 index 0000000..67255b2 --- /dev/null +++ b/wobuzz/mpris/mpris_player.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 + +from jeepney import new_method_return + +from .utils import * + + +class MPRISPlayer(DBusInterface): + def __init__(self, server, interface): + super().__init__(server, interface) + + self.metadata = { + "mpris:trackid": ("o", "/org/mpris/MediaPlayer2/murx"), # random junk, no functionality + "xesam:title": ("s", "Huggenburgl") + } + + def get_all(self): + body = ({ + "CanPlay": ("b", True), + "Metadata": ("a{sv}", self.metadata) + },) + + return body + + def Play(self, msg: Message): + print("Play!") + + def PlayPause(self, msg: Message): + print("Play/Pause!") + + def Metadata(self, msg: Message): + body = (self.metadata,) + + return new_method_return(msg, "a{sv}", body) diff --git a/wobuzz/mpris/mpris_root.py b/wobuzz/mpris/mpris_root.py new file mode 100644 index 0000000..d344e0b --- /dev/null +++ b/wobuzz/mpris/mpris_root.py @@ -0,0 +1,16 @@ +#!/usr/bin/python3 + +from .utils import * + + +class MPRISRoot(DBusInterface): + def get_all(self): + body = ({ + "CanQuit": ("b", True), + "CanRaise": ("b", True) + },) + + return body + + def Raise(self, msg): + print("Raise!") diff --git a/wobuzz/mpris/server.py b/wobuzz/mpris/server.py new file mode 100644 index 0000000..7ab753f --- /dev/null +++ b/wobuzz/mpris/server.py @@ -0,0 +1,68 @@ +#!/usr/bin/python3 + +from threading import Thread +from jeepney.bus_messages import message_bus +from jeepney.io.blocking import DBusConnection, open_dbus_connection + +from .utils import * +from .dbus_properties import DBusProperties +from .mpris_root import MPRISRoot +from .mpris_player import MPRISPlayer + + +class MPRISServer: + def __init__(self, app): + self.app = app + + self.properties_interface = DBusProperties(self, PROPERTIES_INTERFACE) + self.root_interface = MPRISRoot(self, MPRIS_ROOT_INTERFACE) + self.player_interface = MPRISPlayer(self, MPRIS_PLAYER_INTERFACE) + + self.bus_address = DBusAddress(OBJECT_PATH, SERVICE_NAME, PROPERTIES_INTERFACE) + self.bus: DBusConnection = None + + def start(self): + self.bus = open_dbus_connection() + + reply = self.bus.send_and_get_reply(message_bus.RequestName(SERVICE_NAME)) + if reply.body[0] == 1: + print("MPRIS Server connected to DBus: ", SERVICE_NAME) + + Thread(target=self.run_event_loop, daemon=True).start() + + def run_event_loop(self): + while True: + msg = self.bus.receive() + self.app.gui.window.mpris_signal.emit(msg) # queue message in the PyQt event loop + + def handle_event(self, event): # called by app.gui.window.mpris_signal + self.handle_message(event) + + def handle_message(self, msg: Message): + object_path = msg.header.fields[HeaderFields.path] + + if not object_path == OBJECT_PATH: # only accept messages for "/org/mpris/MediaPlayer2" + self.bus.send_message(new_error(msg, *DBusErrors.unknownObject(object_path))) + return + + interface = msg.header.fields[HeaderFields.interface] + + # let the corresponding interface handle the message + if interface == PROPERTIES_INTERFACE: + self.properties_interface.handle_message(msg) + + elif interface == MPRIS_ROOT_INTERFACE: + self.root_interface.handle_message(msg) + + elif interface == MPRIS_PLAYER_INTERFACE: + self.player_interface.handle_message(msg) + + def on_playstate_update(self): + current_playlist = self.app.player.current_playlist + + if current_playlist is not None and current_playlist.current_track is not None: + current_track = current_playlist.current_track + + self.player_interface.metadata["xesam:title"] = ("s", current_track.metadata.title) + + self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE) diff --git a/wobuzz/mpris/utils.py b/wobuzz/mpris/utils.py new file mode 100644 index 0000000..f5a4a55 --- /dev/null +++ b/wobuzz/mpris/utils.py @@ -0,0 +1,96 @@ +#!/usr/bin/python3 + +from jeepney import DBusAddress, Message, MessageType, HeaderFields, new_error, new_signal + + +SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" +OBJECT_PATH = "/org/mpris/MediaPlayer2" +PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" +MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" +MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" + + +class DBusErrors: + @classmethod + def unknownMethod(cls, method: str) -> tuple[str, str, tuple[str]]: + return "org.freedesktop.DBus.Error.UnknownMethod", "s", (f"No such method '{method}'.",) + + @classmethod + def unknownProperty(cls, prop: str) -> tuple[str, str, tuple[str]]: + return "org.freedesktop.DBus.Error.InvalidArgs", "s", (f"No such property '{prop}'",) + + @classmethod + def invalidArgs(cls, prop: str=None, interface: str=None): + if prop is None and interface is None: + return "org.freedesktop.DBus.Error.InvalidArgs" + + if interface is None: + return "org.freedesktop.DBus.Error.InvalidArgs", "s", (f"No such property '{prop}'.",) + + if prop is None: + return "org.freedesktop.DBus.Error.InvalidArgs", "s", (f"No such interface '{interface}'.",) + + return ( + "org.freedesktop.DBus.Error.InvalidArgs", + "s", + (f"No such property '{prop}' on interface '{interface}'.",) + ) + + @classmethod + def unknownObject(cls, path: str): + return "org.freedesktop.DBus.Error.UnknownObject", "s", (f"No such object on path '{path}'.",) + + +class DBusInterface: + def __init__(self, server, interface: str): + self.server = server + self.interface = interface + + def __setattr__(self, key: str, value) -> None: + super().__setattr__(key, value) + + if not key[0].isupper() and not callable(value) and hasattr(self, key.title()): + getter = getattr(self, key.title()) + + if callable(getter): + if hasattr(self.server, "bus"): + self.server.properties_interface.properties_changed(self.interface) + + def handle_message(self, msg: Message): + return_msg = None + + match msg.header.message_type: + case MessageType.method_call: + method_name: str = msg.header.fields[HeaderFields.member] + + if hasattr(self, method_name) and method_name[0].isupper(): + method = getattr(self, msg.header.fields[HeaderFields.member]) + + if callable(method): + return_msg = method(msg) + + else: + return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) + + else: + return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) + + if return_msg is not None: + self.server.bus.send_message(return_msg) + + def get(self, msg: Message): + prop_name: str = msg.body[1] + + if prop_name[0].isupper() and hasattr(self, prop_name): + prop = getattr(self, prop_name) + + if callable(prop): + return prop() + + return new_error(msg, *DBusErrors.unknownProperty(prop_name)) + + else: + return new_error(msg, *DBusErrors.unknownProperty(prop_name)) + + def get_all(self) -> tuple[dict[str: tuple[str: any]]]: + raise NotImplementedError diff --git a/wobuzz/player/mpris.py b/wobuzz/player/mpris.py deleted file mode 100644 index fdf7bf4..0000000 --- a/wobuzz/player/mpris.py +++ /dev/null @@ -1,102 +0,0 @@ -from sdbus import ( - dbus_method_async, - dbus_property_async, - DbusInterfaceCommonAsync, - request_default_bus_name_async -) -import asyncio - -from wobuzz.player.track import TrackMetadata - -SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" -OBJECT_PATH = "/org/mpris/MediaPlayer2" -MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" -MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" - - -class MPRISRoot(DbusInterfaceCommonAsync, interface_name=MPRIS_ROOT_INTERFACE): - @dbus_method_async() - async def Raise(self): - print("Raise, maybe?") - - @dbus_property_async("s") - def Identity(self): - return "Wobuzz" - - -class MPRISPlayer(DbusInterfaceCommonAsync, interface_name=MPRIS_PLAYER_INTERFACE): - def __init__(self): - super().__init__() - - self._metadata = {} - - @dbus_property_async("b") - def CanSeek(self): - return False - - @dbus_method_async() - async def PlayPause(self): - self.app.gui.track_control.toggle_playing_signal.emit() - - @dbus_method_async() - async def Next(self): - self.app.gui.track_control.next_signal.emit() - - @dbus_method_async() - async def Previous(self): - self.app.gui.track_control.previous_signal.emit() - - @dbus_method_async() - async def Stop(self): - self.app.gui.track_control.stop_signal.emit() - - @dbus_property_async("a{sv}") - def Metadata(self): - return self._metadata - - @Metadata.setter # noqa - def Metadata_setter(self, metadata: dict) -> None: - self._metadata = metadata - - async def set_metadata(self, metadata: TrackMetadata): - await self.Metadata.set_async(self.to_xesam(metadata)) - - def to_xesam(self, metadata: "TrackMetadata") -> dict: - # cache name by filename without extension - art_path = self.app.utils.tmp_path + "/cover_cache/" + metadata.path.split("/")[-1][:-4] - - xesam_metadata = { - "mpris:trackid": ("s", "kjuztuktg"), # nonsense, no functionality - "mpris:artUrl": ("s", "file://" + art_path), - "xesam:title": ("s", metadata.title), - "xesam:artist": ("as", [metadata.artist]) - } - - return xesam_metadata - - -class MPRISServer(MPRISRoot, MPRISPlayer): - def __init__(self, app): - super().__init__() - - self.app = app - - self.loop = None - - async def setup_bus(self) -> None: - await request_default_bus_name_async(SERVICE_NAME) - self.export_to_dbus(OBJECT_PATH) - - def start(self): - self.loop = asyncio.new_event_loop() - self.loop.run_until_complete(self.setup_bus()) - self.loop.run_forever() - - def exec_async(self, function): - """ - This stupid function somehow allows us to execute an async function from the main thread. - If someone ha a better solution, please improve this. I have no idea of how asyncio works. - """ - - loop = asyncio.new_event_loop() - loop.run_until_complete(function) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 6946667..452ed72 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -8,7 +8,6 @@ import pygame.event from .playlist import Playlist from .track_progress_timer import TrackProgress -from .mpris import MPRISServer class Player: @@ -24,10 +23,6 @@ class Player: self.history = Playlist(self.app, "History") self.current_playlist = None - self.mpris_server = MPRISServer(self.app) - # start mpris server in a thread (daemon = exit with main thread) - threading.Thread(target=self.mpris_server.start, daemon=True).start() - self.playing = False self.paused = False @@ -43,7 +38,6 @@ class Player: self.app.gui.on_playstate_update() self.export_cover_art_tmp() - self.mpris_server.exec_async(self.mpris_server.set_metadata(self.current_playlist.current_track.metadata)) # cache next track so it immediately starts when the current track finishes self.cache_next_track() diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 35c9e6b..9705b71 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -1,8 +1,10 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QIcon, QShortcut from PyQt6.QtWidgets import QMainWindow, QMenu +from jeepney import Message + from .track_control import TrackControl from .settings import Settings from .process.process_dock import ProcessDock @@ -10,6 +12,8 @@ from .track_info import TrackInfo class MainWindow(QMainWindow): + mpris_signal = pyqtSignal(Message) + def __init__(self, app, gui, parent=None): super().__init__(parent) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 6b41796..d159edc 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -8,11 +8,6 @@ from .track_progress_slider import TrackProgressSlider class TrackControl(QToolBar): - toggle_playing_signal = pyqtSignal() # signals for MPRIS - next_signal = pyqtSignal() - previous_signal = pyqtSignal() - stop_signal = pyqtSignal() - def __init__(self, app, parent=None): super().__init__(parent) @@ -52,13 +47,9 @@ class TrackControl(QToolBar): def connect(self): self.previous_button.triggered.connect(self.previous_track) - self.previous_signal.connect(self.previous_track) self.toggle_play_button.triggered.connect(self.app.player.toggle_playing) - self.toggle_playing_signal.connect(self.app.player.toggle_playing) self.stop_button.triggered.connect(self.app.player.stop) - self.stop_signal.connect(self.app.player.stop) self.next_button.triggered.connect(self.next_track) - self.next_signal.connect(self.next_track) def previous_track(self): if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): @@ -94,4 +85,3 @@ class TrackControl(QToolBar): else: self.toggle_play_button.setIcon(self.play_icon) - diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index cf7a11b..028339c 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -67,10 +67,10 @@ class TrackProgressSlider(QSlider): self.track_control.progress_indicator.setText(self.app.utils.format_time(progress)) - self.track_control.track_progress_slider.setValue(progress) + self.setValue(progress) else: self.track_control.progress_indicator.setText(self.app.utils.format_time(0)) - self.track_control.track_progress_slider.setValue(0) + self.setValue(0) From 1f149a25a30d127209fd0a066f0d90d604aa6b8d Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 18 Apr 2025 19:35:13 +0200 Subject: [PATCH 109/109] MPRIS: Got everything necessary working. (I think.) --- wobuzz/mpris/dbus_introspectable.py | 22 ++++++ wobuzz/mpris/dbus_properties.py | 21 +++--- wobuzz/mpris/introspection.xml | 76 ++++++++++++++++++++ wobuzz/mpris/mpris_player.py | 106 +++++++++++++++++++++++++--- wobuzz/mpris/mpris_root.py | 46 +++++++++++- wobuzz/mpris/server.py | 31 +++++++- wobuzz/mpris/utils.py | 44 +++++++----- wobuzz/player/player.py | 25 ++++++- wobuzz/ui/track_control.py | 1 - wobuzz/ui/track_progress_slider.py | 19 +---- 10 files changed, 333 insertions(+), 58 deletions(-) create mode 100644 wobuzz/mpris/dbus_introspectable.py create mode 100644 wobuzz/mpris/introspection.xml diff --git a/wobuzz/mpris/dbus_introspectable.py b/wobuzz/mpris/dbus_introspectable.py new file mode 100644 index 0000000..3853071 --- /dev/null +++ b/wobuzz/mpris/dbus_introspectable.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 + +from .utils import * + + +class DBUSIntrospectable(DBusInterface): + def __init__(self, server, interface: str): + self.server = server + self.interface = interface + + file = open(server.app.utils.wobuzz_location + "/mpris/introspection.xml") + self.introspection_xml = file.read() + file.close() + + def get_all(self): + body = ({},) + return body + + # ======== Methods ======== + + def Introspect(self, msg): + return new_method_return(msg, "s", (self.introspection_xml,)) diff --git a/wobuzz/mpris/dbus_properties.py b/wobuzz/mpris/dbus_properties.py index 51665f0..d8eefec 100644 --- a/wobuzz/mpris/dbus_properties.py +++ b/wobuzz/mpris/dbus_properties.py @@ -1,9 +1,6 @@ #!/usr/bin/python3 -from jeepney import Header, MessageType, Endianness, MessageFlag, new_method_return -from jeepney.bus_messages import message_bus -from jeepney.io.blocking import open_dbus_connection -from jeepney.wrappers import new_header +from jeepney import new_signal from .utils import * @@ -13,25 +10,31 @@ class DBusProperties(DBusInterface): body = ({},) return body - def properties_changed(self, interface: str): + def properties_changed(self, interface: str, prop_name: str): body = None if interface == MPRIS_ROOT_INTERFACE: - body = (MPRIS_ROOT_INTERFACE,) + self.server.root_interface.get_all() + prop = getattr(self.server.root_interface, prop_name)(None) + + body = (MPRIS_ROOT_INTERFACE,) + ({prop_name: prop}, []) elif interface == MPRIS_PLAYER_INTERFACE: - body = (MPRIS_PLAYER_INTERFACE,) + self.server.player_interface.get_all() + prop = getattr(self.server.player_interface, prop_name)(None) - signature = "" if body is None else "sa{sv}" + body = (MPRIS_PLAYER_INTERFACE,) + ({prop_name: prop}, []) + + signature = "" if body is None else "sa{sv}as" msg = new_signal( - self.server.bus_address.with_interface("org.freedesktop.DBus"), + self.server.bus_address.with_interface(PROPERTIES_INTERFACE), "PropertiesChanged", signature, body ) self.server.bus.send(msg) + # ======== Methods ======== + def Get(self, msg: Message): interface_name = msg.body[0] diff --git a/wobuzz/mpris/introspection.xml b/wobuzz/mpris/introspection.xml new file mode 100644 index 0000000..1fa719c --- /dev/null +++ b/wobuzz/mpris/introspection.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wobuzz/mpris/mpris_player.py b/wobuzz/mpris/mpris_player.py index 67255b2..594722f 100644 --- a/wobuzz/mpris/mpris_player.py +++ b/wobuzz/mpris/mpris_player.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -from jeepney import new_method_return - from .utils import * @@ -9,26 +7,116 @@ class MPRISPlayer(DBusInterface): def __init__(self, server, interface): super().__init__(server, interface) + self.playback_status = "Stopped" + self.loop_status = "None" + self.shuffle = False self.metadata = { - "mpris:trackid": ("o", "/org/mpris/MediaPlayer2/murx"), # random junk, no functionality + "mpris:trackid": ("o", "/org/bla/gubber"), # random junk, no functionality + "mpris:length": ("x", 0), "xesam:title": ("s", "Huggenburgl") } + self.volume = 1.0 def get_all(self): body = ({ + "PlaybackStatus": ("s", self.playback_status), + "LoopStatus": ("s", self.loop_status), + "Rate": ("d", 1.0), + "Shuffle": ("b", self.shuffle), + "Metadata": ("a{sv}", self.metadata), + "Volume": ("d", self.volume), + "Position": ("x", self.server.app.player.get_progress() * 1000), # milliseconds to microseconds + "MinimumRate": ("d", 1.0), + "MaximumRate": ("d", 1.0), + "CanGoNext": ("b", True), + "CanGoPrevious": ("b", True), "CanPlay": ("b", True), - "Metadata": ("a{sv}", self.metadata) + "CanPause": ("b", True), + "CanSeek": ("b", True), + "CanControl": ("b", True) },) return body - def Play(self, msg: Message): - print("Play!") + # ======== Methods ======== + + def Next(self, msg: Message): + self.server.app.player.next_track() + + def Previous(self, msg: Message): + self.server.app.player.previous_track() + + def Pause(self, msg: Message): + return lambda: self.server.app.player.toggle_playing() def PlayPause(self, msg: Message): - print("Play/Pause!") + return lambda: self.server.app.player.toggle_playing() + + def Stop(self, msg: Message): + self.server.app.player.stop() + + def Play(self, msg: Message): + return lambda: self.server.app.player.toggle_playing() + + def Seek(self, msg: Message): + seek_forward = msg.body[0] // 1000 # microseconds to milliseconds + new_position = self.server.app.player.get_progress() + seek_forward + self.server.app.player.seek(new_position) + + def SetPosition(self, msg: Message): + trackid = msg.body[0] + position = msg.body[1] // 1000 # microseconds to milliseconds + + self.server.app.player.seek(position) + + def OpenUri(self, msg: Message): + pass + + # ======== Properties ======== + + def PlaybackStatus(self, msg: Message): + return "s", self.playback_status + + def LoopStatus(self, msg: Message): + return "s", self.loop_status + + def Rate(self, msg: Message): + return "d", 1.0 + + def Shuffle(self, msg: Message): + return "b", self.shuffle def Metadata(self, msg: Message): - body = (self.metadata,) + return "a{sv}", self.metadata + + def Volume(self, msg: Message): + return "d", self.volume + + def Position(self, msg: Message): + return "x", self.server.app.player.get_progress() * 1000 # milliseconds to microseconds + + def MinimumRate(self, msg: Message): + return "d", 1.0 + + def MaximumRate(self, msg: Message): + return "d", 1.0 + + def CanGoNext(self, msg: Message): + return "b", True + + def CanGoPrevious(self, msg: Message): + return "b", True + + def CanPlay(self, msg: Message): + return "b", True + + def CanPause(self, msg: Message): + return "b", True + + def CanSeek(self, msg: Message): + return "b", True + + def CanControl(self, msg: Message): + return "b", True + - return new_method_return(msg, "a{sv}", body) diff --git a/wobuzz/mpris/mpris_root.py b/wobuzz/mpris/mpris_root.py index d344e0b..2d33dc8 100644 --- a/wobuzz/mpris/mpris_root.py +++ b/wobuzz/mpris/mpris_root.py @@ -7,10 +7,52 @@ class MPRISRoot(DBusInterface): def get_all(self): body = ({ "CanQuit": ("b", True), - "CanRaise": ("b", True) + "Fullscreen": ("b", False), + "CanSetFullscreen": ("b", False), + "CanRaise": ("b", True), + "HasTrackList": ("b", False), + "Identity": ("s", "Wobuzz"), + "DesktopEntry": ("s", "wobuzz"), + "SupportedUriSchemes": ("as", ["file"]), + "SupportedMimeTypes": ("as", ["audio/mpeg"]) },) return body + # ======== Methods ======== + def Raise(self, msg): - print("Raise!") + self.server.app.gui.window.activateWindow() + self.server.app.gui.window.showMaximized() + + def Quit(self, msg): + self.server.app.gui.window.close() + + # ======== Properties ======== + + def CanQuit(self, msg: Message): + return "b", True + + def Fullscreen(self, msg: Message): + return "b", False + + def CanSetFullscreen(self, msg: Message): + return "b", False + + def CanRaise(self, msg: Message): + return "b", True + + def HasTrackList(self, msg: Message): + return "b", False + + def Identity(self, msg: Message): + return "s", "Wobuzz" + + def DesktopEntry(self, msg: Message): + return "s", "wobuzz" + + def SupportedUriSchemes(self, msg: Message): + return "as", ["file"] + + def SupportedMimeTypes(self, msg: Message): + return "as", ["audio/mpeg"] diff --git a/wobuzz/mpris/server.py b/wobuzz/mpris/server.py index 7ab753f..9067282 100644 --- a/wobuzz/mpris/server.py +++ b/wobuzz/mpris/server.py @@ -1,11 +1,13 @@ #!/usr/bin/python3 from threading import Thread +from jeepney import DBusAddress from jeepney.bus_messages import message_bus from jeepney.io.blocking import DBusConnection, open_dbus_connection from .utils import * from .dbus_properties import DBusProperties +from .dbus_introspectable import DBUSIntrospectable from .mpris_root import MPRISRoot from .mpris_player import MPRISPlayer @@ -15,6 +17,7 @@ class MPRISServer: self.app = app self.properties_interface = DBusProperties(self, PROPERTIES_INTERFACE) + self.introspection_interface = DBUSIntrospectable(self, INTROSPECTION_INTERFACE) self.root_interface = MPRISRoot(self, MPRIS_ROOT_INTERFACE) self.player_interface = MPRISPlayer(self, MPRIS_PLAYER_INTERFACE) @@ -51,6 +54,9 @@ class MPRISServer: if interface == PROPERTIES_INTERFACE: self.properties_interface.handle_message(msg) + elif interface == INTROSPECTION_INTERFACE: + self.introspection_interface.handle_message(msg) + elif interface == MPRIS_ROOT_INTERFACE: self.root_interface.handle_message(msg) @@ -58,11 +64,30 @@ class MPRISServer: self.player_interface.handle_message(msg) def on_playstate_update(self): - current_playlist = self.app.player.current_playlist + player = self.app.player + current_playlist = player.current_playlist if current_playlist is not None and current_playlist.current_track is not None: current_track = current_playlist.current_track - self.player_interface.metadata["xesam:title"] = ("s", current_track.metadata.title) + art_path = self.app.utils.tmp_path + "/cover_cache/" + current_track.metadata.path.split("/")[-1][:-4] - self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE) + # metadata milli to microseconds --↓ + self.player_interface.metadata["mpris:length"] = ("x", current_track.duration * 1000) + self.player_interface.metadata["mpris:artUrl"] = ("s", "file://" + art_path) + self.player_interface.metadata["xesam:title"] = ("s", current_track.metadata.title) + self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE, "Metadata") + + if player.playing: + if player.paused: + playback_status = "Paused" + + else: + playback_status = "Playing" + + else: + playback_status = "Stopped" + + self.player_interface.playback_status = playback_status + + self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE, "PlaybackStatus") diff --git a/wobuzz/mpris/utils.py b/wobuzz/mpris/utils.py index f5a4a55..6d82d44 100644 --- a/wobuzz/mpris/utils.py +++ b/wobuzz/mpris/utils.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 -from jeepney import DBusAddress, Message, MessageType, HeaderFields, new_error, new_signal - +from jeepney import Message, MessageType, HeaderFields, new_error, new_method_return SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" OBJECT_PATH = "/org/mpris/MediaPlayer2" PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" +INTROSPECTION_INTERFACE = "org.freedesktop.DBus.Introspectable" MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" @@ -46,18 +46,9 @@ class DBusInterface: self.server = server self.interface = interface - def __setattr__(self, key: str, value) -> None: - super().__setattr__(key, value) - - if not key[0].isupper() and not callable(value) and hasattr(self, key.title()): - getter = getattr(self, key.title()) - - if callable(getter): - if hasattr(self.server, "bus"): - self.server.properties_interface.properties_changed(self.interface) - def handle_message(self, msg: Message): return_msg = None + post_action = None # function that gets called after return_msg was set match msg.header.message_type: case MessageType.method_call: @@ -67,7 +58,17 @@ class DBusInterface: method = getattr(self, msg.header.fields[HeaderFields.member]) if callable(method): - return_msg = method(msg) + method_data = method(msg) + + if isinstance(method_data, tuple): + post_action, return_msg = method_data + + else: + if callable(method_data): + post_action = method_data + + else: + return_msg = method_data else: return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) @@ -75,8 +76,13 @@ class DBusInterface: else: return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) - if return_msg is not None: - self.server.bus.send_message(return_msg) + if return_msg is None: + return_msg = new_method_return(msg) + + self.server.bus.send_message(return_msg) + + if not post_action is None: + post_action() def get(self, msg: Message): prop_name: str = msg.body[1] @@ -85,7 +91,13 @@ class DBusInterface: prop = getattr(self, prop_name) if callable(prop): - return prop() + prop_value = prop(msg) + signature = prop_value[0] + value = prop_value[1] + + return_msg = new_method_return(msg, "v", (prop_value,)) + + return return_msg return new_error(msg, *DBusErrors.unknownProperty(prop_name)) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 452ed72..4747cb4 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -35,10 +35,10 @@ class Player: self.playing = True self.paused = False - self.app.gui.on_playstate_update() - self.export_cover_art_tmp() + self.app.gui.on_playstate_update() + # cache next track so it immediately starts when the current track finishes self.cache_next_track() @@ -234,3 +234,24 @@ class Player: file = open(art_path, "wb") file.write(metadata.images.any.data) file.close() + + def get_progress(self) -> int: + """ + Gets the progress of the current track in milliseconds. + (Also when paused.) + :return: Progress in milliseconds + """ + + if self.playing: + remaining = self.track_progress.timer.remainingTime() + + if remaining == -1: + remaining = self.track_progress.remaining_time + + track_duration = self.current_playlist.current_track.duration + + progress = track_duration - remaining + + return progress + + return 0 diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index d159edc..7130789 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -from PyQt6.QtCore import pyqtSignal from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QToolBar, QLabel diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index 028339c..6a35ade 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -55,22 +55,9 @@ class TrackProgressSlider(QSlider): def update_progress(self): if not self.dragged: - if self.app.player.playing: - remaining = self.app.player.track_progress.timer.remainingTime() + progress = self.app.player.get_progress() - if remaining == -1: - remaining = self.app.player.track_progress.remaining_time + self.track_control.progress_indicator.setText(self.app.utils.format_time(progress)) - track_duration = self.app.player.current_playlist.current_track.duration - - progress = track_duration - remaining - - self.track_control.progress_indicator.setText(self.app.utils.format_time(progress)) - - self.setValue(progress) - - else: - self.track_control.progress_indicator.setText(self.app.utils.format_time(0)) - - self.setValue(0) + self.setValue(progress)