Compare commits

...

39 commits
v0.1a1 ... main

Author SHA1 Message Date
fa323a0a87 Added Pip to the requirements. 2025-02-22 18:29:54 +01:00
a23799b6b1 Rearranged some code. 2025-02-22 18:25:17 +01:00
4c0883f694 Added some features that would be cool in the future. 2025-02-21 21:08:05 +01:00
851c2306b4 Fixed another crash that occurred because of an unexpected NoneType in the getting of the playing track's cover. 2025-02-21 20:37:24 +01:00
567afb1866 Fixed another crash by adding a check that makes sure that the current playlist is not NoneType before a current_playlist.has_tracks()-call. 2025-02-21 20:20:35 +01:00
6e7948e579 Added the license also to setup.py 2025-02-21 17:59:27 +01:00
63847f7b42 Changed version to 2nd Alpha of 0.1. 2025-02-21 17:42:38 +01:00
a2e572cf6e Added the "Background Job Monitor" to the list of features. 2025-02-21 17:36:51 +01:00
39bd7e3167 Added a "track_info"-toolbar which shows an image found in the current audio file's metadata (usually the front-cover) and information about the currently playing track such as title and artist name. 2025-02-21 17:26:47 +01:00
ccda6b30c8 Added a "View" submenu to the window's menu and an action to the submenu that opens the background processes. 2025-02-20 19:23:31 +01:00
f2f3937fb2 Fixed a bug where the process widget of the playlist loading thread wouldn't get deleted if the playlist was too short.
The bug occurred because the creation of the widget was done through a PyQt-Signal but the deletion occurred in the same thread as the background process.
2025-02-20 19:17:44 +01:00
301896e12c Added loading of playlists to the process-dock. 2025-02-20 18:55:01 +01:00
6786a3dcd8 Did a little reformatting and removed that commented out test code. xD 2025-02-20 18:06:29 +01:00
a81ea15afd Fixed another crash that occurred because of another player.current_playlist.has_tracks()-check when the current playlist was None. 2025-02-20 17:53:03 +01:00
65564deb82 Added option to load playlists on start to the settings. 2025-02-20 17:44:09 +01:00
5dc91f6605 Implemented that the player stops playing when the deleted playlist is the currently playing. 2025-02-20 17:17:26 +01:00
730e070dfc Fixed a bug by setting Playlist.loaded to True on creation of new playlists so creating new playlists works again. 2025-02-19 19:04:36 +01:00
db191cbc44 Made drag n drop activate just when the playlist is fully loaded. 2025-02-19 18:57:23 +01:00
d8f885959b Added some comments. 2025-02-12 14:48:44 +01:00
f377263a0a Fixed a bug where playlists weren't saved when they weren't loaded from a .m3u. 2025-02-12 14:47:24 +01:00
e5b7ebe6e8 Removed border from playlist titles. (slightly noticeable) 2025-02-12 14:21:59 +01:00
3ac97755bf Set the cursor when hovering over a Playlist title to a normal cursor and made the Player start with the last playlist as active tab. 2025-02-12 14:04:11 +01:00
0c2c91389d Playlists now get loaded when they are started and removed debug prints. 2025-02-12 13:50:12 +01:00
0879575882 Made the double click working again. 2025-02-11 17:34:04 +01:00
22ffd211df Got it working, but it's not better than before... 2025-02-04 14:43:08 +01:00
6134c21ce4 Made it working again. 2025-02-04 13:14:15 +01:00
cf1b4bacd1 Added some checks so the player doesnt crash because of things removed in latest commits. 2025-02-04 11:17:35 +01:00
c164201a55 Just moved some code around. 2025-02-03 18:04:04 +01:00
efe10e7d50 Added a comment. 2025-02-03 18:00:42 +01:00
d36326c029 Replaced shit that got removed in the last commit with something better. 2025-02-03 17:59:37 +01:00
bedca22ca6 Removed some mechanic that is going to be reimplemented. 2025-02-03 17:53:35 +01:00
67d353dcef Removed unused import. 2025-02-03 17:26:00 +01:00
6eac6468a0 Decreased the background progress update rate a little. 2025-02-03 14:12:38 +01:00
c55c1222f0 Added a dock widget that shows background processes. 2025-02-03 14:08:19 +01:00
67c3b9e226 Fixed another crash that occurred because of a missing playlist.has_tracks() check. 2025-02-03 14:06:16 +01:00
95d40dd30c Made a small memory optimisation by making the fonts a class variable and not an instance variable. 2025-02-02 16:08:25 +01:00
563aab6204 Added some comments. 2025-02-02 14:56:06 +01:00
83deb903c1 Removed property that was never used and idk what I wanted to do with it. 2025-02-02 13:47:32 +01:00
2b8969d929 Improved installation instructions. 2025-02-01 15:06:29 +01:00
25 changed files with 488 additions and 121 deletions

View file

@ -5,21 +5,31 @@ Currently, it just has really basic features but many more things are planned.
### Features
| Feature | Description | State |
|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> 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. | <input type="checkbox" disabled /> Not Implemented |
| Feature | Description | State |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> Implemented |
| Background Job Monitor | A QDockWidget where background processes are listed. | <input type="checkbox" disabled checked /> 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. | <input type="checkbox" disabled /> Not Implemented |
| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> Not Implemented |
## Installation
To install Wobuzz, you firstly have to install the dependencies that can't be installed using pip.
This can be done using:
### Release installation
Look at the [Releases](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz/releases),
there you can find the commands that you need for the installation.
### Unstable git installation
You firstly have to install the newest dependencies:
``` bash
sudo apt install xcb libxcb-cursor0 ffmpeg
sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip
```
Now, you can install Wobuzz using just one more command:
Now, you can install the newest unstable version using just one more command:
```bash
pip install wobuzz@git+https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git#egg=wobuzz

View file

@ -19,13 +19,14 @@ long_description = (this_directory / "README.md").read_text()
setuptools.setup(
name="Wobuzz",
version="0.1a1",
version="0.1a2",
description="An audio player made by The Wobbler",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz",
author="The Wobbler",
author_email="emil@i21k.de",
license="GNU GPLv3",
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
package_data={"": ["*.txt", "*.svg"]},
install_requires=[

View file

@ -4,6 +4,8 @@ import os
import sys
import argparse
from wobuzz.player.playlist import Playlist
def main():
description = "A music player made by The Wobbler."
@ -20,23 +22,24 @@ def main():
app = Wobuzz()
if arguments.playlist:
app.library.temporary_playlist.clear()
app.library.temporary_playlist.view.clear()
app.library.temporary_playlist.load_from_m3u(arguments.playlist)
app.library.temporary_playlist.view.load_tracks()
playlist = Playlist(app, "Temporary Playlist", arguments.playlist)
app.library.playlists.append(playlist)
if app.library.temporary_playlist in app.library.playlists:
app.library.playlists.remove(app.library.temporary_playlist)
app.library.temporary_playlist = playlist
if arguments.track:
app.library.temporary_playlist.clear()
app.library.temporary_playlist.view.clear()
playlist = Playlist(app, "Temporary Playlist", arguments.track)
# make track paths absolute
tracks = []
app.library.playlists.append(playlist)
for track in arguments.track:
tracks.append(os.path.abspath(track))
if app.library.temporary_playlist in app.library.playlists:
app.library.playlists.remove(app.library.temporary_playlist)
app.library.temporary_playlist = playlist
app.library.temporary_playlist.load_from_paths(tracks)
app.library.temporary_playlist.view.load_tracks()
app.library.load_playlist_views()
sys.exit(app.qt_app.exec())

View file

@ -11,13 +11,13 @@ class GUI:
self.dropped = []
self.clicked_playlist = self.app.library.temporary_playlist
self.window = MainWindow(app)
self.settings = self.window.settings
self.track_control = self.window.track_control
self.process_dock = self.window.process_dock
self.track_info = self.window.track_info
self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.app.library.main_library_dock)
self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock)
self.app.library.main_library_dock.setFeatures(
QDockWidget.DockWidgetFeature.DockWidgetMovable |
@ -52,5 +52,18 @@ class GUI:
def on_track_change(self, previous_track, track):
self.track_control.on_track_change(previous_track, track)
self.app.player.current_playlist.view.on_track_change(previous_track, track)
for dock_id in self.app.player.current_playlist.views:
view = self.app.player.current_playlist.views[dock_id]
view.on_track_change(previous_track, track)
def on_background_job_start(self, job_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()

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
import os
from PyQt6.QtWidgets import QTabWidget
from PyQt6.QtWidgets import QTabWidget, QAbstractItemView
from ..player.playlist import Playlist
from ..ui.library_dock import LibraryDock
from ..ui.playlist import PlaylistView
@ -18,10 +18,13 @@ class Library:
self.main_library_dock = LibraryDock(self)
self.library_docks = [self.main_library_dock]
self.temporary_playlist = Playlist(self.app, "Temporary Playlist")
self.playlists = [self.temporary_playlist]
self.playlists = []
self.temporary_playlist = None
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):
@ -37,16 +40,11 @@ class Library:
if file_name.endswith(".m3u"):
path = f"{path_playlists}/{file_name}"
if file_name == "Temporary_Playlist.wbz.m3u":
playlist = self.temporary_playlist
playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
self.playlists.append(playlist)
else:
playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
self.playlists.append(playlist)
playlist.load_from_m3u(path)
self.load_playlist_views()
if playlist.title == "Temporary Playlist":
self.temporary_playlist = playlist
def load_playlist_views(self):
for library_dock in self.library_docks:
@ -55,20 +53,35 @@ class Library:
playlist_tabs.playlists = {}
for playlist in self.playlists:
playlist_view = PlaylistView(playlist)
playlist_view = PlaylistView(playlist, library_dock)
playlist_tabs.addTab(playlist_view, playlist.title)
if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened
playlist_tabs.setCurrentIndex(playlist_tabs.count() - 1)
if self.app.settings.load_on_start:
for playlist in self.playlists:
playlist.load()
def on_exit(self, event):
for playlist in self.playlists:
playlist.save()
if playlist.loaded: # only save loaded playlists, unloaded are empty
playlist.save()
if self.app.player.current_playlist is not None:
self.app.settings.latest_playlist = self.app.player.current_playlist.path
def new_playlist(self):
playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist"))
playlist.loaded = True
self.playlists.append(playlist)
for library_dock in self.library_docks:
playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs
playlist_view = PlaylistView(playlist)
playlist_view = PlaylistView(playlist, library_dock)
playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop
playlist_tabs.addTab(playlist_view, playlist.title)

View file

@ -1,6 +1,5 @@
#!/usr/bin/python3
import os
import sys
from PyQt6.QtWidgets import QApplication
from wobbl_tools.data_file import load_dataclass_json
@ -20,14 +19,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.player = Player(self)
self.library = Library(self)
self.player = Player(self)
self.gui = GUI(self)
self.post_init()
self.late_init()
def post_init(self):
self.gui.track_control.track_progress_slider.post_init()
def late_init(self):
self.gui.track_control.track_progress_slider.late_init()
self.library.load()
def on_settings_change(self, key, value):
@ -38,4 +37,7 @@ class Wobuzz:
if __name__ == "__main__":
wobuzz = Wobuzz()
wobuzz.post_init()
wobuzz.library.load_playlist_views()
sys.exit(wobuzz.qt_app.exec())

View file

@ -1,5 +1,6 @@
#!/usr/bin/python3
import time
import threading
import pygame.mixer
import pygame.event
@ -18,7 +19,7 @@ class Player:
self.track_progress = TrackProgress(self.app)
self.history = Playlist(self.app, "History")
self.current_playlist = Playlist(self.app, "None")
self.current_playlist = None
self.playing = False
self.paused = False
@ -32,7 +33,7 @@ class Player:
self.playing = True
self.paused = False
self.app.gui.track_control.on_playstate_update()
self.app.gui.on_playstate_update()
# cache next track so it immediately starts when the current track finishes
self.cache_next_track()
@ -67,7 +68,7 @@ class Player:
self.app.gui.on_track_change(self.history.h_last_track(), self.current_playlist.current_track)
self.app.gui.track_control.on_playstate_update()
self.app.gui.on_playstate_update()
def play_track_in_playlist(self, track_index):
self.stop()
@ -98,7 +99,7 @@ class Player:
self.track_progress.pause()
self.paused = True
self.app.gui.track_control.on_playstate_update()
self.app.gui.on_playstate_update()
def unpause(self):
self.music_channel.unpause()
@ -107,7 +108,7 @@ class Player:
self.playing = True
self.paused = False
self.app.gui.track_control.on_playstate_update()
self.app.gui.on_playstate_update()
def next_track(self):
if not self.current_playlist.on_last_track():
@ -134,13 +135,13 @@ class Player:
self.music_channel.stop()
self.track_progress.stop()
if not self.current_playlist.current_track is None:
if self.current_playlist is not None and self.current_playlist.current_track is not None:
self.current_sound_duration = self.current_playlist.current_track.duration
self.playing = False
self.paused = False
self.app.gui.track_control.on_playstate_update()
self.app.gui.on_playstate_update()
def seek(self, position: int):
self.music_channel.stop()
@ -160,8 +161,15 @@ 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)
@ -170,6 +178,15 @@ class Player:
def start_playlist(self, playlist):
self.stop()
if not playlist.loaded:
playlist.load()
while not playlist.has_tracks() and not playlist.loaded: # wait until first track is loaded
time.sleep(0.1)
if not playlist.has_tracks():
return
self.current_sound, self.current_sound_duration = playlist.set_track(0) # first track
self.current_playlist = playlist

View file

@ -1,15 +1,23 @@
#!/usr/bin/python3
import os
import threading
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QAbstractItemView
from .track import Track
class Playlist:
def __init__(self, app, title: str):
def __init__(self, app, title: str, load_from=None):
self.app = app
self.title = title # playlist title
# if the playlist is imported and not already in the library, this variable will contain the playlist path or
# track path from which the playlist will get imported
# if None, playlist should be already in the library and will be loaded from a .wbz.m3u
self.load_from = load_from
# add to unique names so if the playlist is loaded from disk,
# no other playlist can be created using the same name
self.app.utils.unique_names.append(self.title)
@ -18,7 +26,8 @@ class Playlist:
self.tracks: list[Track] = []
self.current_track_index = 0
self.current_track: Track | None = None
self.view = None
self.views = {} # dict of id(LibraryDock): PlaylistView
self.loaded = False
self.path = os.path.expanduser(
f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
@ -31,19 +40,49 @@ class Playlist:
self.current_track = None
def load_from_paths(self, paths):
num_tracks = len(paths)
i = 0
while i < len(paths):
process_title = f'Loading Playlist "{self.title}"'
self.app.gui.on_background_job_start(
process_title,
f'Loading the tracks of "{self.title}".',
num_tracks,
lambda: i
)
while i < num_tracks:
path = paths[i]
if os.path.isfile(path):
self.tracks.append(Track(self.app, path, cache=i==0)) # first track is cached
self.append_track(Track(self.app, path, cache=i==0)) # first track is cached
i += 1
# set current track to the first track if there is no currently playing track
if self.current_track is None and self.has_tracks():
self.current_track = self.tracks[0]
self.loaded = True
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.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)
for dock_id in self.views: # enable drag and drop on every view
view = self.views[dock_id]
view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
def load_from_m3u(self, path):
file = open(path, "r")
@ -53,9 +92,19 @@ class Playlist:
lines = m3u.split("\n") # m3u entries are separated by newlines
lines = lines[:-1] # remove last entry because it is just an empty string
i = 0
num_lines = len(lines)
i = 0
process_title = f'Loading Playlist "{self.title}"'
self.app.gui.on_background_job_start(
process_title,
f'Loading the tracks of "{self.title}".',
num_lines,
lambda: i
)
while i < num_lines:
line = lines[i]
@ -64,7 +113,7 @@ class Playlist:
continue
self.tracks.append(Track(self.app, line, cache=i==0)) # first track is cached
self.append_track(Track(self.app, line, cache=i==0)) # first track is cached
i += 1
@ -72,10 +121,12 @@ class Playlist:
if self.current_track is None and self.has_tracks():
self.current_track = self.tracks[0]
#self.app.player.history.append_track(self.current_track)
self.loaded = True
self.app.gui.on_background_job_stop(process_title)
def load_from_wbz(self, path):
pass
self.load_from_m3u(path) # placeholder
def has_tracks(self):
return len(self.tracks) > 0
@ -150,11 +201,16 @@ class Playlist:
self.app.utils.unique_names.remove(self.title)
self.app.library.playlists.remove(self)
def append_track(self, track):
self.tracks.append(track)
if self.app.player.current_playlist == self: # stop if this is the current playlist
self.app.player.stop()
self.app.player.current_playlist = None
if self.view:
self.view.append_track(track)
def append_track(self, track):
for dock_id in self.views:
view = self.views[dock_id]
view.append_track(track)
self.tracks.append(track)
def h_last_track(self):
# get last track in history (only gets used in player.history)

View file

@ -10,12 +10,11 @@ class Track:
Class containing data for a track like file path, raw data...
"""
def __init__(self, app, path: str, property_string: str=None, cache: bool=False):
def __init__(self, app, path: str, cache: bool=False):
self.app = app
self.path = path
self.property_string = property_string
self.tags = TinyTag.get(self.path, ignore_errors=False, duration=False)
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False)
self.cached = False
self.audio = None
@ -34,6 +33,7 @@ class Track:
new_occurrences = {}
for item in self.items:
# create dict of item: item.index (actually the id of the item bc. the item can't be used as key)
playlist_occurrences = new_occurrences.get(item.playlist, {})
playlist_occurrences[id(item)] = item.index
@ -48,13 +48,14 @@ class Track:
If this track is the currently playing track, and it gets moved, this corrects the current playlist index.
"""
if self.app.player.current_playlist.current_track is self:
for item in self.items:
if (
item.playlist in self.occurrences and
self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index
):
self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)])
if self.app.player.current_playlist is not None:
if self.app.player.current_playlist.current_track is self:
for item in self.items:
if (
item.playlist in self.occurrences and
self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index
):
self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)])
def cache(self):
self.load_audio()
@ -66,6 +67,8 @@ class Track:
self.duration = len(self.audio) # track duration in milliseconds
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images
self.cached = True
def clear_cache(self):
@ -75,6 +78,8 @@ class Track:
self.sound = None
self.duration = 0
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images
def load_audio(self):
#file_type = self.path.split(".")[-1]

View file

@ -30,5 +30,5 @@ class TrackProgress:
def stop(self):
self.timer.stop()
if not self.app.player.current_playlist.current_track is None:
if self.app.player.current_playlist is not None and self.app.player.current_playlist.current_track is not None:
self.remaining_time = self.app.player.current_playlist.current_track.duration

View file

@ -9,4 +9,6 @@ class Settings:
window_maximized: bool=False
library_path: str="~/.wobuzz"
clear_track_cache: bool=True
latest_playlist: str=None
load_on_start: bool=True

View file

@ -11,12 +11,6 @@ class LibraryDock(QDockWidget):
self.library = library
self.setAllowedAreas(
Qt.DockWidgetArea.LeftDockWidgetArea |
Qt.DockWidgetArea.RightDockWidgetArea |
Qt.DockWidgetArea.BottomDockWidgetArea
)
self.setAcceptDrops(True)
self.library_widget = Library(library, self)

View file

@ -5,6 +5,8 @@ from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMainWindow, QMenu
from .track_control import TrackControl
from .settings import Settings
from .process.process_dock import ProcessDock
from .track_info import TrackInfo
class MainWindow(QMainWindow):
@ -28,6 +30,11 @@ class MainWindow(QMainWindow):
self.settings_action = self.edit_menu.addAction("&Settings")
self.view_menu = QMenu("&View", self.menu_bar)
self.menu_bar.addMenu(self.view_menu)
self.processes_action = self.view_menu.addAction("Show &Background Processes")
self.track_control = TrackControl(app)
self.addToolBar(self.track_control)
@ -35,5 +42,13 @@ class MainWindow(QMainWindow):
self.settings.hide()
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.settings)
self.settings_action.triggered.connect(self.settings.show)
self.process_dock = ProcessDock(app)
self.process_dock.hide()
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.process_dock)
self.track_info = TrackInfo(app)
self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.track_info)
self.settings_action.triggered.connect(self.settings.show)
self.processes_action.triggered.connect(self.process_dock.show)

View file

@ -2,7 +2,7 @@
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QDropEvent, QIcon, QFont
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
from .track import TrackItem
@ -10,25 +10,22 @@ from .track import TrackItem
class PlaylistView(QTreeWidget):
itemDropped = pyqtSignal(QTreeWidget, list)
def __init__(self, playlist, parent=None):
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
def __init__(self, playlist, dock, parent=None):
super().__init__(parent)
self.playlist = playlist
self.library_dock = dock
self.app = playlist.app
playlist.view = self
playlist.views[id(dock)] = self
self.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",
@ -39,8 +36,6 @@ class PlaylistView(QTreeWidget):
self.setHeaderLabels(headers)
self.load_tracks()
self.itemActivated.connect(self.on_track_activation)
def on_user_sort(self):
@ -63,7 +58,8 @@ class PlaylistView(QTreeWidget):
i += 1
if self.app.player.current_playlist.has_tracks():
# make sure the next track is cached (could be moved by user)
if self.app.player.current_playlist == self.playlist and self.app.player.current_playlist.has_tracks():
self.app.player.cache_next_track()
def dropEvent(self, event: QDropEvent):
@ -133,20 +129,14 @@ class PlaylistView(QTreeWidget):
# unmark the previous track in all playlists
for item in previous_track.items:
item.setIcon(0, QIcon(None))
item.setFont(1, self.normal_font)
item.setFont(2, self.normal_font)
item.setFont(3, self.normal_font)
item.unmark()
if track:
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
# mark the current track in this playlist
item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
item.setIcon(0, self.playing_mark)
item.setFont(1, self.bold_font)
item.setFont(2, self.bold_font)
item.setFont(3, self.normal_font)
item.mark()
def append_track(self, track):
TrackItem(track, self.topLevelItemCount() - 1, self)

View file

@ -42,8 +42,7 @@ class PlaylistTabBar(QTabBar):
playlist = playlist_view.playlist
if playlist.has_tracks(): # dont crash when playlist is empty
self.app.player.start_playlist(playlist)
self.app.player.start_playlist(playlist)
def contextMenuEvent(self, event: QContextMenuEvent, title=None):
# get title by self.tabAt() if the event is called from PyQt, else its executed from the tab title and getting

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QMouseEvent
from PyQt6.QtGui import QMouseEvent, QCursor
from PyQt6.QtWidgets import QLineEdit
from .tab_bar import PlaylistTabBar
@ -16,10 +16,11 @@ class TabTitle(QLineEdit):
self.index = index
self.playlist_view = playlist_view
self.setStyleSheet("QLineEdit {background: transparent;}")
self.setStyleSheet("QLineEdit {background: transparent; border: none;}")
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor)
self.editingFinished.connect(self.on_edit)
def mouseDoubleClickEvent(self, event: QMouseEvent):

View file

@ -0,0 +1 @@
#!/usr/bin/python3

View file

@ -0,0 +1,41 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QSizePolicy, QGroupBox, QLabel, QProgressBar, QVBoxLayout
class BackgroundProcess(QGroupBox):
normal_font = QFont()
normal_font.setBold(False)
bold_font = QFont()
bold_font.setBold(True)
def __init__(self, title: str, parent=None, description: str="", steps: int=0):
super().__init__(title, parent)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter)
self.setFont(self.bold_font)
self.layout = QVBoxLayout(self)
self.description = QLabel(description, self)
self.description.setFont(self.normal_font)
self.layout.addWidget(self.description)
self.progress_bar = QProgressBar(self)
self.progress_bar.setMaximum(steps)
self.layout.addWidget(self.progress_bar)
self.setLayout(self.layout)
def get_progress(self):
return 0
def set_range(self, maximum: int, minimum: int=0):
self.progress_bar.setRange(minimum, maximum)
def update_progress(self):
self.progress_bar.setValue(self.get_progress())

View file

@ -0,0 +1,75 @@
#!/usr/bin/python3
from PyQt6.QtCore import QTimer, pyqtSignal
from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout
from .process import BackgroundProcess
PROGRESS_UPDATE_RATE = 10
PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE
class ProcessDock(QDockWidget):
# we need a signal for self.on_background_job_start() because PyQt6 doesn't allow some operations to be performed
# 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.progress_update_timer = QTimer()
self.progress_update_timer.timeout.connect(self.update_processes)
self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL)
self.job_started_signal.connect(self.on_background_job_start)
self.job_finished_signal.connect(self.on_background_job_stop)
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()

View file

@ -10,5 +10,8 @@ class BehaviourSettings(QWidget):
self.layout = QFormLayout(self)
self.setLayout(self.layout)
self.load_on_start = QCheckBox(self)
self.layout.addRow("Load playlists on start", self.load_on_start)
self.clear_track_cache = QCheckBox(self)
self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache)
self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache)

View file

@ -44,6 +44,7 @@ class Settings(QDockWidget):
def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event
self.file_settings.library_path_input.setText(self.app.settings.library_path)
self.behavior_settings.clear_track_cache.setChecked(self.app.settings.clear_track_cache)
self.behavior_settings.load_on_start.setChecked(self.app.settings.load_on_start)
def update_settings(self, key, value):
match key:
@ -53,7 +54,11 @@ class Settings(QDockWidget):
case "clear_track_cache":
self.behavior_settings.clear_track_cache.setDown(value)
case "load_on_start":
self.behavior_settings.load_on_start.setChecked(value)
def write_settings(self):
self.app.settings.library_path = self.file_settings.library_path_input.text()
self.app.settings.clear_track_cache = self.behavior_settings.clear_track_cache.isChecked()
self.app.settings.load_on_start = self.behavior_settings.load_on_start.isChecked()

View file

@ -1,20 +1,31 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QIcon, QPalette
from PyQt6.QtWidgets import QTreeWidgetItem
class TrackItem(QTreeWidgetItem):
normal_font = QFont()
bold_font = QFont()
bold_font.setBold(True)
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
def __init__(self, track, index, parent=None):
super().__init__(parent)
self.track = track
self.index_user_sort = index
self.index = index
self.playlist = parent.playlist
palette = parent.palette()
self.highlight_color = palette.color(QPalette.ColorRole.Highlight)
self.base_color = palette.color(QPalette.ColorRole.Base)
track.items.append(self)
track.set_occurrences()
@ -31,3 +42,16 @@ class TrackItem(QTreeWidgetItem):
self.setText(3, track.tags.album)
self.setText(4, str(self.index_user_sort + 1))
def mark(self):
self.setIcon(0, self.playing_mark)
self.setFont(1, self.bold_font)
self.setFont(2, self.bold_font)
self.setFont(3, self.normal_font)
def unmark(self):
self.setIcon(0, QIcon(None))
self.setFont(1, self.normal_font)
self.setFont(2, self.normal_font)
self.setFont(3, self.normal_font)

View file

@ -46,15 +46,15 @@ class TrackControl(QToolBar):
self.next_button.triggered.connect(self.next_track)
def previous_track(self):
if self.app.player.current_playlist.has_tracks():
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
self.app.player.previous_track()
def stop(self):
if self.app.player.current_playlist.has_tracks():
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
self.app.player.stop()
def next_track(self):
if self.app.player.current_playlist.has_tracks():
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
self.app.player.next_track()
def on_track_change(self, previous_track, track):
@ -80,11 +80,17 @@ class TrackControl(QToolBar):
elif self.app.player.playing: # playing
self.app.player.pause()
elif self.app.player.current_playlist.has_tracks(): # stopped but tracks in the current playlist
# stopped but tracks in the current playlist
elif self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
self.app.player.start_playing()
elif self.app.player.current_playlist.title == "None":
self.app.player.start_playlist(self.app.gui.clicked_playlist)
elif self.app.player.current_playlist is None:
if self.app.settings.latest_playlist is not None:
for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist
if playlist.path == self.app.settings.latest_playlist:
self.app.player.start_playlist(playlist)
break
def on_playstate_update(self):
if self.app.player.playing:

91
wobuzz/ui/track_info.py Normal file
View file

@ -0,0 +1,91 @@
#!/usr/bin/python3
from PyQt6.QtGui import QPixmap, QFont
from PyQt6.QtWidgets import QToolBar, QWidget, QLabel, QSizePolicy, QVBoxLayout
class TrackInfo(QToolBar):
title_font = QFont()
title_font.setPointSize(16)
title_font.setBold(True)
artist_font = QFont()
title_font.setPointSize(12)
album_font = QFont()
album_font.setPointSize(8)
def __init__(self, app, parent=None):
super().__init__(parent)
self.app = app
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.wobuzz_logo = QPixmap(f"{self.app.utils.wobuzz_location}/icon.svg")
self.track_cover = QLabel(self)
self.track_cover.setFixedSize(64, 64)
self.track_cover.setScaledContents(True)
self.track_cover.setPixmap(self.wobuzz_logo)
self.addWidget(self.track_cover)
self.info_container = QWidget(self)
info_container_layout = QVBoxLayout(self.info_container)
self.info_container.setLayout(info_container_layout)
self.addWidget(self.info_container)
self.title = QLabel("Title", self.info_container)
self.title.setFont(self.title_font)
info_container_layout.addWidget(self.title)
self.artist = QLabel("Artist", self.info_container)
self.artist.setFont(self.artist_font)
info_container_layout.addWidget(self.artist)
self.album = QLabel("Album", self.info_container)
self.album.setFont(self.album_font)
info_container_layout.addWidget(self.album)
def update_info(self):
current_playlist = self.app.player.current_playlist
if current_playlist is not None:
current_track = current_playlist.current_track
title = current_track.tags.title
artist = current_track.tags.artist
album = current_track.tags.album
self.title.setText(title)
if artist is not None and not artist == "":
self.artist.setText(f"By {current_track.tags.artist}")
else:
self.artist.setText("No Artist")
if album is not None and not album == "":
self.album.setText(f"In {current_track.tags.album}")
else:
self.album.setText("No Album")
cover = current_track.tags.images.any
if cover is None:
self.track_cover.setPixmap(self.wobuzz_logo)
return
cover_data = cover.data
if isinstance(cover_data, bytes):
cover_pixmap = QPixmap()
cover_pixmap.loadFromData(cover_data)
self.track_cover.setPixmap(cover_pixmap)
else:
self.track_cover.setPixmap(self.wobuzz_logo)

View file

@ -29,7 +29,7 @@ class TrackProgressSlider(QSlider):
self.sliderPressed.connect(self.on_press)
self.sliderReleased.connect(self.on_release)
def post_init(self):
def late_init(self):
self.track_control = self.app.gui.track_control
def mousePressEvent(self, event: QMouseEvent):
@ -57,7 +57,7 @@ class TrackProgressSlider(QSlider):
def on_release(self):
self.dragged = False
if self.app.player.current_playlist.has_tracks():
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
self.app.player.seek(self.value())
def update_progress(self):