diff --git a/.gitignore b/.gitignore
index 9119131..4a3d3dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
wobuzz/settings.json
Wobuzz.egg-info
-build
__pycache__
.idea
\ No newline at end of file
diff --git a/README.md b/README.md
index 789a27e..698e5fb 100644
--- a/README.md
+++ b/README.md
@@ -2,76 +2,29 @@
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
-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.
-
-### 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 |
-| 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
-
-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.
+| 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 |
## 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),
-there you can find the commands that you need for the installation.
-
-### Unstable git installation
-
-You firstly have to install the newest dependencies:
+To install Wobuzz, you firstly have to install the dependencies that can't be installed using pip.
+This can be done using:
``` bash
-sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip git
+sudo apt install pyqt6-dev-tools xcb libxcb-cursor0 ffmpeg
```
-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
-```
-
-### 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.
+Now you can just clone the repo and let pip install it.
``` bash
git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git
cd Wobuzz
-pip install -e .
+pip install .
```
## Usage:
diff --git a/requirements.txt b/requirements.txt
index 7981831..0cad0aa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,4 @@
-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
-setuptools~=78.1.0
-Wobuzz~=0.1a3
-jeepney~=0.8.0
\ No newline at end of file
+PyQt6
+pygame
+tinytag
+pydub
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 5df990e..e86c545 100644
--- a/setup.py
+++ b/setup.py
@@ -1,41 +1,29 @@
#!/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(
name="Wobuzz",
- version="0.1a3",
+ version="0.0",
description="An audio player made by The Wobbler",
long_description=long_description,
long_description_content_type="text/markdown",
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"]},
+ package_data={"": ["*.txt"]},
install_requires=[
- "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"
+ "PyQt6",
+ "tinytag",
+ "pydub",
+ "pygame",
+ "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@main#egg=wobbl_tools"
],
entry_points={
"console_scripts": ["wobuzz=wobuzz.command_line:main"],
diff --git a/wobuzz.desktop b/wobuzz.desktop
deleted file mode 100644
index 6936e52..0000000
--- a/wobuzz.desktop
+++ /dev/null
@@ -1,10 +0,0 @@
-[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/command_line.py b/wobuzz/command_line.py
index 71925fc..cf9f842 100644
--- a/wobuzz/command_line.py
+++ b/wobuzz/command_line.py
@@ -4,8 +4,6 @@ import os
import sys
import argparse
-from wobuzz.player.playlist import Playlist
-
def main():
description = "A music player made by The Wobbler."
@@ -22,14 +20,23 @@ def main():
app = Wobuzz()
if arguments.playlist:
- playlist = Playlist(app, "Temporary Playlist", arguments.playlist)
-
- app.library.replace_temporary_playlist(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()
if arguments.track:
- app.library.open_tracks(arguments.track)
+ app.library.temporary_playlist.clear()
+ app.library.temporary_playlist.view.clear()
- app.library.load_playlist_views()
+ # 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()
sys.exit(app.qt_app.exec())
diff --git a/wobuzz/gui.py b/wobuzz/gui.py
index a56082a..5bcf85f 100644
--- a/wobuzz/gui.py
+++ b/wobuzz/gui.py
@@ -1,9 +1,8 @@
#!/usr/bin/python3
-from PyQt6.QtCore import QTimer
-
+from PyQt6.QtCore import Qt
+from PyQt6.QtWidgets import QDockWidget
from .ui.main_window import MainWindow
-from .ui.popups import Popups
class GUI:
@@ -12,36 +11,35 @@ class GUI:
self.dropped = []
- self.window = MainWindow(app, self)
+ self.clicked_playlist = self.app.library.temporary_playlist
+
+ self.window = MainWindow(app)
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.popups = Popups(app, self)
+ self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.app.library.main_library_dock)
- self.window.setCentralWidget(self.app.library.main_library_widget)
+ self.app.library.main_library_dock.setFeatures(
+ QDockWidget.DockWidgetFeature.DockWidgetMovable |
+ QDockWidget.DockWidgetFeature.DockWidgetFloatable
+ )
if self.app.settings.window_maximized:
self.window.showMaximized()
- elif self.app.settings.window_size is not None:
+ elif not self.app.settings.window_size is None:
self.window.resize(*self.app.settings.window_size)
- 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)
-
- self.window.closeEvent = self.on_exit
+ self.connect()
self.window.show()
self.settings.update_all()
- def on_exit(self, event):
- self.window.focusWidget().clearFocus() # clear focus on focused widget
+ def connect(self):
+ self.window.closeEvent = self.on_exit
- self.app.player.stop()
+ def on_exit(self, event):
self.app.library.on_exit(event)
self.app.settings.window_size = (self.window.width(), self.window.height())
@@ -52,32 +50,7 @@ class GUI:
def on_settings_change(self, key, value):
self.settings.update_settings(key, 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)
+ self.app.player.current_playlist.view.on_track_change(previous_track, track)
- 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):
- self.process_dock.job_started_signal.emit(job_name, description, steps, getter)
-
- 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()
- self.app.mpris_server.on_playstate_update()
-
- 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/icon.svg b/wobuzz/icon.svg
deleted file mode 100644
index 98885cb..0000000
--- a/wobuzz/icon.svg
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
diff --git a/wobuzz/library/__init__.py b/wobuzz/library/__init__.py
index 8fc6dc1..a93a4bf 100644
--- a/wobuzz/library/__init__.py
+++ b/wobuzz/library/__init__.py
@@ -1,8 +1 @@
#!/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 a9c8c14..482df6f 100644
--- a/wobuzz/library/library.py
+++ b/wobuzz/library/library.py
@@ -1,12 +1,10 @@
#!/usr/bin/python3
import os
-from PyQt6.QtWidgets import QTabWidget, QAbstractItemView
-
+from PyQt6.QtWidgets import QTabWidget
from ..player.playlist import Playlist
-from ..ui.library.library import LibraryWidget
-from ..ui.playlist_view import PlaylistView
-from ..types import Types
+from ..ui.library_dock import LibraryDock
+from ..ui.playlist import PlaylistView
class Library:
@@ -17,20 +15,13 @@ class Library:
def __init__(self, app):
self.app = app
- self.main_library_widget = LibraryWidget(self)
- self.library_widgets = [self.main_library_widget]
+ self.main_library_dock = LibraryDock(self)
+ self.library_docks = [self.main_library_dock]
- self.loaded_tracks = {} # dict of {track path: track}
-
- self.playlists = []
- self.temporary_playlist = None
-
- self.artist_playlists = []
+ self.temporary_playlist = Playlist(self.app, "Temporary Playlist")
+ self.playlists = [self.temporary_playlist]
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):
@@ -46,137 +37,38 @@ class Library:
if file_name.endswith(".m3u"):
path = f"{path_playlists}/{file_name}"
- playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
- self.playlists.append(playlist)
+ if file_name == "Temporary_Playlist.wbz.m3u":
+ playlist = self.temporary_playlist
- if playlist.title == "Temporary Playlist":
- self.temporary_playlist = playlist
+ else:
+ playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
+ self.playlists.append(playlist)
+
+ playlist.load_from_m3u(path)
+
+ self.load_playlist_views()
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
+ playlist_tabs.playlists = {}
- # create view for each playlist
for playlist in self.playlists:
- if id(library_widget) in playlist.views: # view already exists
- continue
-
- playlist_view = PlaylistView(playlist, library_widget)
+ playlist_view = PlaylistView(playlist)
playlist_tabs.addTab(playlist_view, playlist.title)
- 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()
-
def on_exit(self, event):
for playlist in self.playlists:
- 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
+ playlist.save()
def new_playlist(self):
playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist"))
- playlist.loaded = True
-
self.playlists.append(playlist)
- for library_widget in self.library_widgets:
- playlist_tabs: QTabWidget = library_widget.playlist_tabs
-
- playlist_view = PlaylistView(playlist, library_widget, playlist_tabs)
- playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop
+ for library_dock in self.library_docks:
+ playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs
+ playlist_view = PlaylistView(playlist)
playlist_tabs.addTab(playlist_view, playlist.title)
- def replace_temporary_playlist(self, replace: Playlist):
- if self.temporary_playlist is not None:
- self.temporary_playlist.delete()
-
- 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()
-
- 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, 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]}"
-
- if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library
- return
-
- track.copy(new_track_path, import_options.copy_type)
-
- def open_playlist(self, playlist_path: str):
- playlist = Playlist(self.app, "Temporary Playlist", playlist_path)
-
- self.replace_temporary_playlist(playlist)
-
- self.load_playlist_views()
-
- playlist.load()
-
- def import_playlist(self, playlist_path: str, import_options):
- playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_options)
-
- self.replace_temporary_playlist(playlist)
-
- self.load_playlist_views()
-
- playlist.load()
-
- 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/main.py b/wobuzz/main.py
index 14331a8..ce67244 100644
--- a/wobuzz/main.py
+++ b/wobuzz/main.py
@@ -1,5 +1,6 @@
#!/usr/bin/python3
+import os
import sys
from PyQt6.QtWidgets import QApplication
from wobbl_tools.data_file import load_dataclass_json
@@ -8,7 +9,6 @@ from .utils import Utils
from .player import Player
from .library.library import Library
from .gui import GUI
-from .mpris import MPRISServer
class Wobuzz:
@@ -20,17 +20,14 @@ 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.library = Library(self)
self.player = Player(self)
+ self.library = Library(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()
+ self.post_init()
- def late_init(self):
- self.gui.track_control.track_progress_slider.late_init()
+ def post_init(self):
+ self.gui.track_control.track_progress_slider.post_init()
self.library.load()
def on_settings_change(self, key, value):
@@ -41,7 +38,4 @@ 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/mpris/__init__.py b/wobuzz/mpris/__init__.py
deleted file mode 100644
index a5a4d67..0000000
--- a/wobuzz/mpris/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/python3
-
-from .server import MPRISServer
-
diff --git a/wobuzz/mpris/dbus_introspectable.py b/wobuzz/mpris/dbus_introspectable.py
deleted file mode 100644
index 3853071..0000000
--- a/wobuzz/mpris/dbus_introspectable.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/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
deleted file mode 100644
index d8eefec..0000000
--- a/wobuzz/mpris/dbus_properties.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/usr/bin/python3
-
-from jeepney import new_signal
-
-from .utils import *
-
-
-class DBusProperties(DBusInterface):
- def get_all(self):
- body = ({},)
- return body
-
- def properties_changed(self, interface: str, prop_name: str):
- body = None
-
- if interface == MPRIS_ROOT_INTERFACE:
- prop = getattr(self.server.root_interface, prop_name)(None)
-
- body = (MPRIS_ROOT_INTERFACE,) + ({prop_name: prop}, [])
-
- elif interface == MPRIS_PLAYER_INTERFACE:
- prop = getattr(self.server.player_interface, prop_name)(None)
-
- 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(PROPERTIES_INTERFACE),
- "PropertiesChanged",
- signature,
- body
- )
- self.server.bus.send(msg)
-
- # ======== Methods ========
-
- 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/introspection.xml b/wobuzz/mpris/introspection.xml
deleted file mode 100644
index 1fa719c..0000000
--- a/wobuzz/mpris/introspection.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wobuzz/mpris/mpris_player.py b/wobuzz/mpris/mpris_player.py
deleted file mode 100644
index 594722f..0000000
--- a/wobuzz/mpris/mpris_player.py
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/usr/bin/python3
-
-from .utils import *
-
-
-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/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),
- "CanPause": ("b", True),
- "CanSeek": ("b", True),
- "CanControl": ("b", True)
- },)
-
- return body
-
- # ======== 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):
- 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):
- 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
-
-
diff --git a/wobuzz/mpris/mpris_root.py b/wobuzz/mpris/mpris_root.py
deleted file mode 100644
index 2d33dc8..0000000
--- a/wobuzz/mpris/mpris_root.py
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/python3
-
-from .utils import *
-
-
-class MPRISRoot(DBusInterface):
- def get_all(self):
- body = ({
- "CanQuit": ("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):
- 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
deleted file mode 100644
index 9067282..0000000
--- a/wobuzz/mpris/server.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/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
-
-
-class MPRISServer:
- def __init__(self, app):
- 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)
-
- 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 == INTROSPECTION_INTERFACE:
- self.introspection_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):
- 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
-
- art_path = self.app.utils.tmp_path + "/cover_cache/" + current_track.metadata.path.split("/")[-1][:-4]
-
- # 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
deleted file mode 100644
index 6d82d44..0000000
--- a/wobuzz/mpris/utils.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/python3
-
-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"
-
-
-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 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:
- 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):
- 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))
-
- else:
- return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name))
-
- 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]
-
- if prop_name[0].isupper() and hasattr(self, prop_name):
- prop = getattr(self, prop_name)
-
- if callable(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))
-
- 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/player.py b/wobuzz/player/player.py
index 4747cb4..24261a4 100644
--- a/wobuzz/player/player.py
+++ b/wobuzz/player/player.py
@@ -1,11 +1,8 @@
#!/usr/bin/python3
-import os
-import time
import threading
import pygame.mixer
import pygame.event
-
from .playlist import Playlist
from .track_progress_timer import TrackProgress
@@ -21,7 +18,7 @@ class Player:
self.track_progress = TrackProgress(self.app)
self.history = Playlist(self.app, "History")
- self.current_playlist = None
+ self.current_playlist = Playlist(self.app, "None")
self.playing = False
self.paused = False
@@ -35,9 +32,7 @@ class Player:
self.playing = True
self.paused = False
- self.export_cover_art_tmp()
-
- self.app.gui.on_playstate_update()
+ self.app.gui.track_control.on_playstate_update()
# cache next track so it immediately starts when the current track finishes
self.cache_next_track()
@@ -72,7 +67,7 @@ class Player:
self.app.gui.on_track_change(self.history.h_last_track(), self.current_playlist.current_track)
- self.app.gui.on_playstate_update()
+ self.app.gui.track_control.on_playstate_update()
def play_track_in_playlist(self, track_index):
self.stop()
@@ -93,7 +88,7 @@ class Player:
if (
self.app.settings.clear_track_cache and
- last_track is not None and
+ not last_track is None and
not last_track == self.current_playlist.current_track
):
last_track.clear_cache()
@@ -103,7 +98,7 @@ class Player:
self.track_progress.pause()
self.paused = True
- self.app.gui.on_playstate_update()
+ self.app.gui.track_control.on_playstate_update()
def unpause(self):
self.music_channel.unpause()
@@ -112,7 +107,7 @@ class Player:
self.playing = True
self.paused = False
- self.app.gui.on_playstate_update()
+ self.app.gui.track_control.on_playstate_update()
def next_track(self):
if not self.current_playlist.on_last_track():
@@ -139,13 +134,13 @@ class Player:
self.music_channel.stop()
self.track_progress.stop()
- if self.current_playlist is not None and self.current_playlist.current_track is not None:
+ if not self.current_playlist.current_track is None:
self.current_sound_duration = self.current_playlist.current_track.duration
self.playing = False
self.paused = False
- self.app.gui.on_playstate_update()
+ self.app.gui.track_control.on_playstate_update()
def seek(self, position: int):
self.music_channel.stop()
@@ -165,15 +160,8 @@ 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(
- "Loading Track",
- "Loading next track in the background so it starts immediately."
- )
-
track.cache()
- 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
caching_thread = threading.Thread(target=self.caching_thread_function)
@@ -182,76 +170,7 @@ 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
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
-
- 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()
-
- 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/player/playlist.py b/wobuzz/player/playlist.py
index 0bba6a7..1160128 100644
--- a/wobuzz/player/playlist.py
+++ b/wobuzz/player/playlist.py
@@ -1,47 +1,24 @@
#!/usr/bin/python3
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
+from .track import Track
class Playlist:
- def __init__(self, app, title: str, load_from=None, import_options: Types.ImportOptions=None):
+ def __init__(self, app, title: str):
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
-
- 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
self.app.utils.unique_names.append(self.title)
- # 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.sorting: list[Qt.SortOrder] | None = None # Custom sort order if None
self.tracks: list[Track] = []
self.current_track_index = 0
self.current_track: Track | None = None
- self.views = {} # dict of id(LibraryWidget): PlaylistView
- self.loaded = False
- self.loading = False
-
- self.path = self.path_from_title(title)
+ self.view = None
def clear(self):
self.sorting: list[Qt.SortOrder] | None = None
@@ -50,60 +27,19 @@ class Playlist:
self.current_track = None
def load_from_paths(self, paths):
- num_tracks = len(paths)
-
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_tracks,
- lambda: i
- )
-
- while i < num_tracks:
+ while i < len(paths):
path = paths[i]
if os.path.isfile(path):
- self.append_track(Track(self.app, path, cache=i==0)) # first track is cached
+ self.tracks.append(Track(self.app, path, cache=i==0)) # first track is cached
i += 1
- 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()
-
- def loading_thread(self):
- 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)
-
- 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): # if it's created from tracks
- self.load_from_paths(self.load_from)
-
- self.loading = False
-
- if self.import_options is not None:
- for track in self.tracks:
- 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]
-
- view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
+ # 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]
def load_from_m3u(self, path):
file = open(path, "r")
@@ -113,18 +49,8 @@ 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
- 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
- )
+ num_lines = len(lines)
while i < num_lines:
line = lines[i]
@@ -134,7 +60,7 @@ class Playlist:
continue
- self.append_track(Track(self.app, line, cache=i==0)) # first track is cached
+ self.tracks.append(Track(self.app, line, cache=i==0)) # first track is cached
i += 1
@@ -142,110 +68,10 @@ class Playlist:
if self.current_track is None and self.has_tracks():
self.current_track = self.tracks[0]
- self.loaded = True
-
- self.app.gui.on_background_job_stop(process_title)
+ #self.app.player.history.append_track(self.current_track)
def load_from_wbz(self, path):
- 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
- )
-
- wbzm3u = WobuzzM3U(self.path)
- track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U
-
- while i < num_lines:
- line = lines[i]
-
- line_data = wbzm3u.parse_line(line)
-
- 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
-
- self.sorting.append(line_data)
-
- if isinstance(line_data, WBZM3UData.TrackMetadata.TrackTitle):
- track_metadata.title = line_data
-
- if isinstance(line_data, WBZM3UData.TrackMetadata.TrackArtist):
- track_metadata.artist = line_data
-
- if isinstance(line_data, WBZM3UData.TrackMetadata.TrackAlbum):
- track_metadata.album = line_data
-
- i += 1
-
- continue
-
- elif isinstance(line_data, WBZM3UData.URL): # ignore urls
- i += 1
-
- continue
-
- 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
-
- # 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()
-
- 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()
+ pass
def has_tracks(self):
return len(self.tracks) > 0
@@ -288,86 +114,54 @@ class Playlist:
return self.current_track.sound, self.current_track.duration
def save(self):
- first_view = list(self.views.values())[0]
- first_view.sortItems(5, Qt.SortOrder.AscendingOrder) # sort by custom sorting
- self.sync(first_view)
-
- wbzm3u = WobuzzM3U(self.path)
-
wbz_data = ""
- wbz_data += wbzm3u.assemble_line(WBZM3UData.Header)
-
- for order in self.sorting:
- wbz_data += wbzm3u.assemble_line(order)
-
for track in self.tracks:
- # cache track metadata
- 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 = open(
+ os.path.expanduser(
+ f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
+ ),
+ "w"
+ )
wbz.write(wbz_data)
wbz.close()
def rename(self, title: str):
- if os.path.exists(self.path):
- os.remove(self.path)
+ # 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))
old_title = self.title
self.title = self.app.utils.unique_name(title, ignore=old_title)
- 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:
- 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)
def delete(self):
- if os.path.exists(self.path):
- os.remove(self.path)
+ path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
+ path = os.path.expanduser(path)
- 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()
-
- 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 is self.app.library.temporary_playlist:
- self.app.library.temporary_playlist = None
+ if os.path.exists(path):
+ os.remove(os.path.expanduser(path))
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]
- view.append_track(track)
-
self.tracks.append(track)
+ if self.view:
+ self.view.append_track(track)
+
def h_last_track(self):
# get last track in history (only gets used in player.history)
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 faf0f8b..f80e1d2 100644
--- a/wobuzz/player/track.py
+++ b/wobuzz/player/track.py
@@ -1,71 +1,29 @@
#!/usr/bin/python3
-import os
-import shutil
from pydub import AudioSegment
+from pydub.effects import normalize
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:
- path: str | None=None
- 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):
- # 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.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 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
+SUPPORTED_FORMATS = [
+ "mp3",
+ "wav",
+ "ogg"
+]
class Track:
"""
- Class representing a track.
+ Class containing data for a track like file path, raw data...
"""
- def __init__(self, app, path: str, cache: bool=False, metadata: TrackMetadata=None):
+ def __init__(self, app, path: str, property_string: str=None, cache: bool=False):
self.app = app
self.path = path
+ self.property_string = property_string
- # 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)
-
- self.metadata = TrackMetadata(path, tags.title, tags.artist, tags.album)
-
- else:
- self.metadata = metadata
+ self.tags = TinyTag.get(self.path, ignore_errors=False, duration=False)
self.cached = False
self.audio = None
@@ -73,30 +31,17 @@ class Track:
self.duration = 0
self.items = []
- self.occurrences = {} # all occurrences in playlists categorized by playlist and id of the track widget
+ self.occurrences = {} # all occurrences in playlists categorized by playlist and track widget
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
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
@@ -111,14 +56,13 @@ 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 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)])
+ 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()
@@ -130,9 +74,6 @@ class Track:
self.duration = len(self.audio) # track duration in milliseconds
- # metadata with images
- self.metadata.images = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True).images
-
self.cached = True
def clear_cache(self):
@@ -142,10 +83,11 @@ class Track:
self.sound = None
self.duration = 0
- self.metadata.images = None
-
def load_audio(self):
- self.audio = AudioSegment.from_file(self.path)
+ file_type = self.path.split(".")[-1]
+
+ if file_type in SUPPORTED_FORMATS:
+ self.audio = AudioSegment.from_file(self.path)
def remaining(self, position: int):
remaining_audio = self.audio[position:]
@@ -156,29 +98,3 @@ 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)
-
- 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/player/track_progress_timer.py b/wobuzz/player/track_progress_timer.py
index 0ee77a6..2a5a66b 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 self.app.player.current_playlist is not None and self.app.player.current_playlist.current_track is not None:
+ if not self.app.player.current_playlist.current_track is None:
self.remaining_time = self.app.player.current_playlist.current_track.duration
diff --git a/wobuzz/settings.py b/wobuzz/settings.py
index f13e0dd..46e7a81 100644
--- a/wobuzz/settings.py
+++ b/wobuzz/settings.py
@@ -9,8 +9,4 @@ class Settings:
window_maximized: bool=False
library_path: str="~/.wobuzz"
clear_track_cache: bool=True
- latest_playlist: str=None
- load_on_start: bool=False
- gui_update_rate: int=20
- album_cover_size: int=64
diff --git a/wobuzz/types/__init__.py b/wobuzz/types/__init__.py
deleted file mode 100644
index 052adfa..0000000
--- a/wobuzz/types/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/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
deleted file mode 100644
index ceed274..0000000
--- a/wobuzz/types/import_options.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/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
deleted file mode 100644
index dbbab54..0000000
--- a/wobuzz/types/types.py
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/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
deleted file mode 100644
index 6cfdb5a..0000000
--- a/wobuzz/ui/custom_widgets/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/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
deleted file mode 100644
index 8b61620..0000000
--- a/wobuzz/ui/custom_widgets/group_box.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/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/library.py b/wobuzz/ui/library.py
similarity index 85%
rename from wobuzz/ui/library/library.py
rename to wobuzz/ui/library.py
index 302ac77..a269b4b 100644
--- a/wobuzz/ui/library/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, QToolButton
-from wobuzz.ui.playlist_tabs import PlaylistTabs
+from PyQt6.QtWidgets import QToolBox, QLabel, QTabWidget, QToolButton
+from .playlist_tabs import PlaylistTabs
-class LibraryWidget(QToolBox):
+class Library(QToolBox):
def __init__(self, library, parent=None):
super().__init__(parent)
diff --git a/wobuzz/ui/library/__init__.py b/wobuzz/ui/library/__init__.py
deleted file mode 100644
index a93a4bf..0000000
--- a/wobuzz/ui/library/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-#!/usr/bin/python3
diff --git a/wobuzz/ui/library/artist_view.py b/wobuzz/ui/library/artist_view.py
deleted file mode 100644
index a444491..0000000
--- a/wobuzz/ui/library/artist_view.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/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/import_dialog.py b/wobuzz/ui/library/import_dialog.py
deleted file mode 100644
index 5612515..0000000
--- a/wobuzz/ui/library/import_dialog.py
+++ /dev/null
@@ -1,117 +0,0 @@
-#!/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.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)
- 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/library/library_dock.py b/wobuzz/ui/library_dock.py
similarity index 57%
rename from wobuzz/ui/library/library_dock.py
rename to wobuzz/ui/library_dock.py
index f1362de..825d6d3 100644
--- a/wobuzz/ui/library/library_dock.py
+++ b/wobuzz/ui/library_dock.py
@@ -1,7 +1,8 @@
#!/usr/bin/python3
+from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QDockWidget
-from wobuzz.ui.library import LibraryWidget
+from .library import Library
class LibraryDock(QDockWidget):
@@ -10,6 +11,12 @@ 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 9705b71..f8aa7ac 100644
--- a/wobuzz/ui/main_window.py
+++ b/wobuzz/ui/main_window.py
@@ -1,49 +1,28 @@
#!/usr/bin/python3
-from PyQt6.QtCore import Qt, pyqtSignal
-from PyQt6.QtGui import QIcon, QShortcut
+from PyQt6.QtCore import Qt
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
-from .track_info import TrackInfo
class MainWindow(QMainWindow):
- mpris_signal = pyqtSignal(Message)
-
- def __init__(self, app, gui, parent=None):
+ def __init__(self, app, parent=None):
super().__init__(parent)
self.app = app
- self.gui = gui
-
- self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg")
self.setWindowTitle("Wobuzz")
- self.setWindowIcon(self.icon)
self.menu_bar = self.menuBar()
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 Tracks")
-
- self.playlist_menu = QMenu("&Playlist", self.menu_bar)
- self.menu_bar.addMenu(self.playlist_menu)
-
- 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)
- self.view_menu = QMenu("&View", self.menu_bar)
- self.menu_bar.addMenu(self.view_menu)
+ self.settings_action = self.edit_menu.addAction("&Settings")
self.track_control = TrackControl(app)
self.addToolBar(self.track_control)
@@ -52,16 +31,5 @@ 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)
- self.track_info = TrackInfo(app)
- self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.track_info)
-
- 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/playlist.py b/wobuzz/ui/playlist.py
new file mode 100644
index 0000000..0a4c047
--- /dev/null
+++ b/wobuzz/ui/playlist.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python3
+
+from PyQt6.QtCore import pyqtSignal
+from PyQt6.QtGui import QDropEvent, QIcon, QFont
+from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame
+
+from .track import TrackItem
+
+
+class PlaylistView(QTreeWidget):
+ itemDropped = pyqtSignal(QTreeWidget, list)
+
+ def __init__(self, playlist, parent=None):
+ super().__init__(parent)
+
+ self.playlist = playlist
+ self.app = playlist.app
+
+ 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)
+
+ self.setColumnCount(4)
+
+ self.playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
+
+ headers = [
+ "#",
+ "Title",
+ "Artist",
+ "Album",
+ "# Custom Sorting"
+ ]
+
+ self.setHeaderLabels(headers)
+
+ self.load_tracks()
+
+ self.itemActivated.connect(self.on_track_activation)
+
+ def on_user_sort(self):
+ 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()
+
+ i += 1
+
+ if self.app.player.current_playlist.has_tracks():
+ self.app.player.cache_next_track()
+
+ def dropEvent(self, event: QDropEvent):
+ # receive items that were dropped and create new items from its tracks (new items bc. widgets can only have
+ # one parent)
+ if event.source() == self:
+ items = self.selectedItems() # dragged items are always selected items
+
+ self.itemDropped.emit(self, items)
+
+ else:
+ items = self.app.gui.dropped
+
+ i = 0
+
+ for item in items:
+ track = item.track
+
+ self.playlist.tracks.append(track)
+
+ track_item = TrackItem(track, i, self)
+
+ i += 1
+
+ super().dropEvent(event)
+
+ event.accept()
+
+ self.on_user_sort()
+
+ def dragEnterEvent(self, event):
+ # store dragged items in gui.dropped, so the other playlist can receive it
+ if event.source() == self:
+ items = self.selectedItems()
+
+ self.app.gui.dropped = items
+
+ super().dragEnterEvent(event)
+
+ event.accept()
+
+ def load_tracks(self):
+ i = 0
+
+ for track in self.playlist.tracks:
+ track_item = TrackItem(track, i, self)
+
+ i += 1
+
+ def on_track_activation(self, item, column):
+ if not self.app.player.current_playlist == self.playlist:
+ self.app.player.current_playlist = self.playlist
+
+ index = self.indexOfTopLevelItem(item)
+ self.app.player.play_track_in_playlist(index)
+
+ def on_track_change(self, previous_track, track):
+ # unmark the previous track and playlist and mark the current track and playlist as playing
+
+ playlist_tabs = self.parent().parent()
+ index = playlist_tabs.indexOf(self) # tab index of this playlist
+
+ if previous_track:
+ # unmark all playlists by looping through the tabs
+ for i in range(playlist_tabs.count()):
+ playlist_tabs.setTabIcon(i, QIcon(None))
+
+ # 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)
+
+ 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)
+
+ def append_track(self, track):
+ TrackItem(track, self.topLevelItemCount() - 1, self)
+
diff --git a/wobuzz/ui/playlist_tabs/tab_bar.py b/wobuzz/ui/playlist_tabs/tab_bar.py
index ed50ba8..afcfd5f 100644
--- a/wobuzz/ui/playlist_tabs/tab_bar.py
+++ b/wobuzz/ui/playlist_tabs/tab_bar.py
@@ -32,17 +32,10 @@ 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):
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
self.app.player.start_playlist(playlist)
@@ -58,7 +51,7 @@ class PlaylistTabBar(QTabBar):
if index == -1: # when no tab was clicked, do nothing
return
- playlist_view = self.tab_widget.widget(index)
- playlist = playlist_view.playlist
+ title = self.tabButton(index, QTabBar.ButtonPosition.RightSide)
+
+ self.context_menu.exec(event.globalPos(), title)
- 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 b235f9d..6e285a2 100644
--- a/wobuzz/ui/playlist_tabs/tab_context_menu.py
+++ b/wobuzz/ui/playlist_tabs/tab_context_menu.py
@@ -4,8 +4,6 @@ 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):
@@ -13,8 +11,7 @@ class PlaylistContextMenu(QMenu):
self.tab_bar: QTabBar = parent
- self.tab_index = -1
- self.playlist = None
+ self.playlist_title = None
self.title = self.addSection("Playlist Actions")
@@ -27,20 +24,17 @@ 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
+ def exec(self, pos: QPoint, title):
+ self.playlist_title = title
- self.title.setText(playlist.title)
+ self.title.setText(title.text()) # set section title
super().exec(pos)
def rename(self):
- # 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)
+ self.playlist_title.setFocus()
def delete(self):
- self.playlist.delete()
+ self.playlist_title.playlist_view.playlist.delete()
+ self.playlist_title.playlist_view.deleteLater()
+
diff --git a/wobuzz/ui/playlist_tabs/tab_title.py b/wobuzz/ui/playlist_tabs/tab_title.py
new file mode 100644
index 0000000..bcae3e5
--- /dev/null
+++ b/wobuzz/ui/playlist_tabs/tab_title.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python3
+
+from PyQt6.QtCore import Qt
+from PyQt6.QtGui import QMouseEvent
+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;}")
+
+ self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
+
+ 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
deleted file mode 100644
index 3b020d7..0000000
--- a/wobuzz/ui/playlist_tabs/tab_title_editor.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/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 4976458..9eb286e 100644
--- a/wobuzz/ui/playlist_tabs/tab_widget.py
+++ b/wobuzz/ui/playlist_tabs/tab_widget.py
@@ -1,8 +1,9 @@
#!/usr/bin/python3
-from PyQt6.QtWidgets import QTabWidget
+from PyQt6.QtWidgets import QTabWidget, QTabBar
from .tab_bar import PlaylistTabBar
+from .tab_title import TabTitle
class PlaylistTabs(QTabWidget):
@@ -18,3 +19,13 @@ class PlaylistTabs(QTabWidget):
self.setMovable(True)
self.setAcceptDrops(True)
+
+ def addTab(self, widget, label):
+ super().addTab(widget, None)
+
+ index = self.tab_bar.count() - 1
+
+ title = TabTitle(self.app, label, self.tab_bar, index, widget)
+
+ self.tab_bar.setTabButton(index, QTabBar.ButtonPosition.RightSide, title)
+
diff --git a/wobuzz/ui/playlist_view.py b/wobuzz/ui/playlist_view.py
deleted file mode 100644
index b9e3958..0000000
--- a/wobuzz/ui/playlist_view.py
+++ /dev/null
@@ -1,193 +0,0 @@
-#!/usr/bin/python3
-
-from PyQt6.QtCore import Qt, pyqtSignal
-from PyQt6.QtGui import QDropEvent, QIcon
-from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
-
-from .track import TrackItem
-from ..wobuzzm3u import WBZM3UData
-
-
-class PlaylistView(QTreeWidget):
- itemDropped = pyqtSignal(QTreeWidget, list)
- sort_signal = pyqtSignal(int, Qt.SortOrder)
-
- playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
-
- def __init__(self, playlist, library_widget, parent=None):
- super().__init__(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(5)
-
- headers = [
- "#",
- "Title",
- "Artist",
- "Album",
- "Genre",
- "# Custom Sorting"
- ]
-
- 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 on_header_click(self, section_index: int):
- if section_index == 0: # this would just invert the current sorting
- return
-
- sorting = self.playlist.sorting
- last_order = sorting[4]
-
- 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] = order # set sorting
-
- # convert True/False to Qt.SortOrder.AscendingOrder/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
-
- # 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 order in self.playlist.sorting:
- # convert True/False to Qt.SortOrder.AscendingOrder/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(order.sort_by + 1, qorder)
- # 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 = self.topLevelItem(i)
-
- i += 1
-
- track.setText(0, str(i)) # 0 = index
-
- if user_sort:
- track.setText(5, str(i)) # 5 = user sort index
-
- if 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(WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True))
-
- self.header.setSortIndicator(5, Qt.SortOrder.AscendingOrder)
-
- 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
- # one parent)
- if event.source() == self:
- items = self.selectedItems() # dragged items are always selected items
-
- self.itemDropped.emit(self, items)
-
- else:
- items = self.app.gui.dropped
-
- i = 0
-
- for item in items:
- track = item.track
-
- self.playlist.tracks.append(track)
-
- track_item = TrackItem(track, i, self)
-
- i += 1
-
- super().dropEvent(event)
-
- event.accept()
-
- self.on_sort(True)
-
- def dragEnterEvent(self, event):
- # store dragged items in gui.dropped, so the other playlist can receive it
- if event.source() == self:
- items = self.selectedItems()
-
- self.app.gui.dropped = items
-
- super().dragEnterEvent(event)
-
- event.accept()
-
- def load_tracks(self):
- i = 0
-
- for track in self.playlist.tracks:
- track_item = TrackItem(track, i, self)
-
- i += 1
-
- def on_track_activation(self, item, column):
- if not self.app.player.current_playlist == self.playlist:
- self.app.player.current_playlist = self.playlist
-
- index = self.indexOfTopLevelItem(item)
- self.app.player.play_track_in_playlist(index)
-
- def on_track_change(self, previous_track, track):
- # unmark the previous track and playlist and mark the current track and playlist as playing
-
- playlist_tabs = self.parent().parent()
- index = playlist_tabs.indexOf(self) # tab index of this playlist
-
- if previous_track:
- # unmark all playlists by looping through the tabs
- for i in range(playlist_tabs.count()):
- playlist_tabs.setTabIcon(i, QIcon(None))
-
- # unmark the previous track in all playlists
- for item in previous_track.items:
- 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.mark()
-
- def append_track(self, track):
- TrackItem(track, self.topLevelItemCount(), self)
-
diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py
deleted file mode 100644
index 7b7ec13..0000000
--- a/wobuzz/ui/popups.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/python3
-
-from PyQt6.QtWidgets import QDialog, QFileDialog
-
-from .library.import_dialog import ImportDialog
-from ..types import Types
-
-
-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 *.m4a)", "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.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)
- 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 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 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()
-
- if playlist_path is not None and not playlist_path == "":
- self.app.library.open_playlist(playlist_path)
-
- def import_playlist(self):
- playlist_path = self.select_playlist_file()
-
- 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/process/__init__.py b/wobuzz/ui/process/__init__.py
deleted file mode 100644
index a93a4bf..0000000
--- a/wobuzz/ui/process/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-#!/usr/bin/python3
diff --git a/wobuzz/ui/process/process.py b/wobuzz/ui/process/process.py
deleted file mode 100644
index 80e5fcf..0000000
--- a/wobuzz/ui/process/process.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/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
deleted file mode 100644
index a310fd4..0000000
--- a/wobuzz/ui/process/process_dock.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/python3
-
-from PyQt6.QtCore import Qt, pyqtSignal
-from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout
-
-from .process import BackgroundProcess
-
-
-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)
-
- 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.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:
- 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_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:
- self.processes.pop(job).deleteLater()
-
diff --git a/wobuzz/ui/settings/__init__.py b/wobuzz/ui/settings/__init__.py
index 0ffb38f..1212f64 100644
--- a/wobuzz/ui/settings/__init__.py
+++ b/wobuzz/ui/settings/__init__.py
@@ -1,3 +1,3 @@
#!/usr/bin/python3
-from .settings import Settings
+from .settings import Settings
\ No newline at end of file
diff --git a/wobuzz/ui/settings/behavior.py b/wobuzz/ui/settings/behavior.py
new file mode 100644
index 0000000..569714e
--- /dev/null
+++ b/wobuzz/ui/settings/behavior.py
@@ -0,0 +1,14 @@
+#!/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.clear_track_cache = QCheckBox(self)
+ self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache)
\ No newline at end of file
diff --git a/wobuzz/ui/settings/category.py b/wobuzz/ui/settings/category.py
deleted file mode 100644
index 9932946..0000000
--- a/wobuzz/ui/settings/category.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/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/file.py b/wobuzz/ui/settings/file.py
new file mode 100644
index 0000000..f380f94
--- /dev/null
+++ b/wobuzz/ui/settings/file.py
@@ -0,0 +1,17 @@
+#!/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 5678b82..b2809a1 100644
--- a/wobuzz/ui/settings/settings.py
+++ b/wobuzz/ui/settings/settings.py
@@ -1,20 +1,9 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import (
- QWidget,
- QDockWidget,
- QTabWidget,
- QLineEdit,
- QCheckBox,
- QPushButton,
- QSpinBox,
- QVBoxLayout,
- QSizePolicy
-)
-
-from .category import Category
-from .sub_category import SubCategory
+from PyQt6.QtWidgets import QWidget, QDockWidget, QTabWidget, QPushButton, QVBoxLayout
+from .file import FileSettings
+from .behavior import BehaviourSettings
class Settings(QDockWidget):
@@ -38,77 +27,12 @@ class Settings(QDockWidget):
self.tabs = QTabWidget(self.content)
self.content_layout.addWidget(self.tabs)
- self.file_settings = Category()
-
- self.file_settings.paths = SubCategory("Paths")
- 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.file_settings = FileSettings()
self.tabs.addTab(self.file_settings, "Files")
- self.behavior_settings = Category()
-
- 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",)
- 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.behavior_settings = BehaviourSettings()
self.tabs.addTab(self.behavior_settings, "Behavior")
- self.appearance_settings = Category()
-
- 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()
- 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")
- # self.performance_settings.add_sub_category(self.performance_settings.memory)
-
- self.performance_settings.cpu = SubCategory("CPU",)
- 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.\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")
-
self.save_button = QPushButton("&Save", self.content)
self.content_layout.addWidget(self.save_button)
@@ -118,33 +42,18 @@ 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.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)
- self.appearance_settings.track_info.cover_size.setValue(self.app.settings.album_cover_size)
+ 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)
def update_settings(self, key, value):
match key:
case "library_path":
- self.file_settings.paths.library_path_input.setText(value)
+ self.file_settings.library_path_input.setText(value)
case "clear_track_cache":
- self.behavior_settings.track.clear_cache.setDown(value)
-
- 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)
-
- case "track_cover_size":
- self.appearance_settings.track_info.cover_size.setValue(value)
+ self.behavior_settings.clear_track_cache.setDown(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()
+ 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()
diff --git a/wobuzz/ui/settings/sub_category.py b/wobuzz/ui/settings/sub_category.py
deleted file mode 100644
index 6453b78..0000000
--- a/wobuzz/ui/settings/sub_category.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/python3
-
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QFont
-from PyQt6.QtWidgets import QLabel, QSizePolicy, QFormLayout
-
-from ..custom_widgets import GroupBox
-
-
-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.layout = QFormLayout()
- self.setLayout(self.layout)
-
- 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.replace("\n", "\n "))
- description_label.setFont(self.description_font)
- self.layout.addRow(description_label)
-
diff --git a/wobuzz/ui/track.py b/wobuzz/ui/track.py
index aa73ecc..38ec961 100644
--- a/wobuzz/ui/track.py
+++ b/wobuzz/ui/track.py
@@ -1,32 +1,20 @@
#!/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.parent = parent
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()
@@ -38,34 +26,8 @@ class TrackItem(QTreeWidgetItem):
)
self.setText(0, str(self.index + 1))
- self.setText(1, track.metadata.title)
- self.setText(2, track.metadata.artist)
- self.setText(3, track.metadata.album)
- 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.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 == 5:
- return int(self.text(column)) < int(other.text(column))
-
- else:
- return super().__lt__(other)
+ self.setText(1, track.tags.title)
+ self.setText(2, track.tags.artist)
+ self.setText(3, track.tags.album)
+ self.setText(4, str(self.index_user_sort + 1))
diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py
index 7130789..76fb391 100644
--- a/wobuzz/ui/track_control.py
+++ b/wobuzz/ui/track_control.py
@@ -2,7 +2,6 @@
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QToolBar, QLabel
-
from .track_progress_slider import TrackProgressSlider
@@ -19,18 +18,14 @@ 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)
@@ -46,16 +41,20 @@ class TrackControl(QToolBar):
def connect(self):
self.previous_button.triggered.connect(self.previous_track)
- self.toggle_play_button.triggered.connect(self.app.player.toggle_playing)
- self.stop_button.triggered.connect(self.app.player.stop)
+ self.toggle_play_button.triggered.connect(self.toggle_playing)
+ self.stop_button.triggered.connect(self.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():
+ if self.app.player.current_playlist.has_tracks():
self.app.player.previous_track()
+ def stop(self):
+ if 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():
+ if self.app.player.current_playlist.has_tracks():
self.app.player.next_track()
def on_track_change(self, previous_track, track):
@@ -74,6 +73,19 @@ 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()
+
+ elif self.app.player.current_playlist.has_tracks(): # stopped but tracks in the current playlist
+ self.app.player.start_playing()
+
+ elif self.app.player.current_playlist.title == "None":
+ self.app.player.start_playlist(self.app.gui.clicked_playlist)
+
def on_playstate_update(self):
if self.app.player.playing:
if self.app.player.paused:
@@ -84,3 +96,4 @@ class TrackControl(QToolBar):
else:
self.toggle_play_button.setIcon(self.play_icon)
+
diff --git a/wobuzz/ui/track_info.py b/wobuzz/ui/track_info.py
deleted file mode 100644
index 0be8010..0000000
--- a/wobuzz/ui/track_info.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/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.setWindowTitle("Track Info")
- 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.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)
-
- 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.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
-
- if current_playlist is not None and current_playlist.current_track is not None:
- current_track = current_playlist.current_track
- 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 {artist}")
-
- else:
- self.artist.setText("")
-
- if album is not None and not album == "":
- self.album.setText(f"In {album}")
-
- else:
- 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: # can't display cover image when there 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)
-
- self.track_cover.setPixmap(cover_pixmap)
-
- 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)
-
- def set_size(self, size: int):
- self.track_cover.setFixedSize(size, size)
-
diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py
index 6a35ade..bc476d0 100644
--- a/wobuzz/ui/track_progress_slider.py
+++ b/wobuzz/ui/track_progress_slider.py
@@ -1,9 +1,12 @@
#!/usr/bin/python3
-from PyQt6.QtCore import Qt
+from PyQt6.QtCore import Qt, QTimer
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):
@@ -14,6 +17,10 @@ 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()
@@ -22,7 +29,7 @@ class TrackProgressSlider(QSlider):
self.sliderPressed.connect(self.on_press)
self.sliderReleased.connect(self.on_release)
- def late_init(self):
+ def post_init(self):
self.track_control = self.app.gui.track_control
def mousePressEvent(self, event: QMouseEvent):
@@ -50,14 +57,27 @@ class TrackProgressSlider(QSlider):
def on_release(self):
self.dragged = False
- if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
+ if self.app.player.current_playlist.has_tracks():
self.app.player.seek(self.value())
def update_progress(self):
if not self.dragged:
- progress = self.app.player.get_progress()
+ if self.app.player.playing:
+ remaining = self.app.player.track_progress.timer.remainingTime()
- self.track_control.progress_indicator.setText(self.app.utils.format_time(progress))
+ if remaining == -1:
+ remaining = self.app.player.track_progress.remaining_time
- self.setValue(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.track_control.track_progress_slider.setValue(progress)
+
+ else:
+ self.track_control.progress_indicator.setText(self.app.utils.format_time(0))
+
+ self.track_control.track_progress_slider.setValue(0)
diff --git a/wobuzz/utils.py b/wobuzz/utils.py
index eabba71..901b57b 100644
--- a/wobuzz/utils.py
+++ b/wobuzz/utils.py
@@ -8,7 +8,6 @@ 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
diff --git a/wobuzz/wobuzzm3u/__init__.py b/wobuzz/wobuzzm3u/__init__.py
deleted file mode 100644
index 54b7ee8..0000000
--- a/wobuzz/wobuzzm3u/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/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
deleted file mode 100644
index 5d1c49c..0000000
--- a/wobuzz/wobuzzm3u/wbzm3u_data.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/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
deleted file mode 100644
index b9891f4..0000000
--- a/wobuzz/wobuzzm3u/wobuzzm3u.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/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 data is WBZM3UData.Header or 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"