forked from Wobbl/Wobuzz
Compare commits
54 commits
Author | SHA1 | Date | |
---|---|---|---|
3424b6ed97 | |||
fa323a0a87 | |||
a23799b6b1 | |||
4c0883f694 | |||
851c2306b4 | |||
567afb1866 | |||
6e7948e579 | |||
63847f7b42 | |||
a2e572cf6e | |||
39bd7e3167 | |||
ccda6b30c8 | |||
f2f3937fb2 | |||
301896e12c | |||
6786a3dcd8 | |||
a81ea15afd | |||
65564deb82 | |||
5dc91f6605 | |||
730e070dfc | |||
db191cbc44 | |||
d8f885959b | |||
f377263a0a | |||
e5b7ebe6e8 | |||
3ac97755bf | |||
0c2c91389d | |||
0879575882 | |||
22ffd211df | |||
6134c21ce4 | |||
cf1b4bacd1 | |||
c164201a55 | |||
efe10e7d50 | |||
d36326c029 | |||
bedca22ca6 | |||
67d353dcef | |||
6eac6468a0 | |||
c55c1222f0 | |||
67c3b9e226 | |||
95d40dd30c | |||
563aab6204 | |||
83deb903c1 | |||
2b8969d929 | |||
b2a40d0087 | |||
af4f267377 | |||
85dfa412d0 | |||
ea23f0e127 | |||
8dddeac673 | |||
429ec8e683 | |||
f0215c034a | |||
1fefc76dd7 | |||
453e1b75b8 | |||
060132be36 | |||
ed92c46f95 | |||
b77df6987c | |||
8d502afcee | |||
753ff66bf2 |
29 changed files with 642 additions and 152 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
wobuzz/settings.json
|
||||
Wobuzz.egg-info
|
||||
build
|
||||
__pycache__
|
||||
.idea
|
42
README.md
42
README.md
|
@ -5,26 +5,50 @@ 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 pyqt6-dev-tools xcb libxcb-cursor0 ffmpeg
|
||||
sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip git
|
||||
```
|
||||
|
||||
Now you can just clone the repo and let pip install it.
|
||||
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.
|
||||
|
||||
``` bash
|
||||
git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git
|
||||
cd Wobuzz
|
||||
pip install .
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage:
|
||||
|
|
|
@ -2,3 +2,4 @@ PyQt6
|
|||
pygame
|
||||
tinytag
|
||||
pydub
|
||||
wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools
|
19
setup.py
19
setup.py
|
@ -1,29 +1,40 @@
|
|||
#!/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.0",
|
||||
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"]},
|
||||
package_data={"": ["*.txt", "*.svg"]},
|
||||
install_requires=[
|
||||
"PyQt6",
|
||||
"tinytag",
|
||||
"pydub",
|
||||
"pygame",
|
||||
"wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@main#egg=wobbl_tools"
|
||||
"wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools"
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": ["wobuzz=wobuzz.command_line:main"],
|
||||
|
|
10
wobuzz.desktop
Normal file
10
wobuzz.desktop
Normal file
|
@ -0,0 +1,10 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Wobuzz
|
||||
Comment=A simple audio player
|
||||
Keywords=audio;music;player;wobuzz;wobbl;qt;
|
||||
Categories=Multimedia;Media;AudioVideo;Audio;Player;Music;Qt;
|
||||
Exec=wobuzz %F
|
||||
Icon=wobuzz
|
||||
Terminal=false
|
||||
MimeType=audio/mpeg;audio/x-wav;audio/x-flac;audio/x-aiff;audio/x-m4a;audio/x-ms-wma;audio/x-vorbis+ogg;
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
90
wobuzz/icon.svg
Normal file
90
wobuzz/icon.svg
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 270.93333 270.93333"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.75989759"
|
||||
inkscape:cx="421.10938"
|
||||
inkscape:cy="458.61443"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1023"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1075"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#6d758a;fill-opacity:1;stroke:none;stroke-width:0.367058px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 182.77204,41.828827 c 26.1142,-31.9117865 53.72304,-45.9799062 62.67325,-40.7688755 8.53202,4.9675519 4.87612,41.4855685 -14.96576,76.6633855 -5.53567,9.814241 -55.03746,-26.937222 -47.70749,-35.89451 z"
|
||||
id="path6997"
|
||||
sodipodi:nodetypes="ssss" />
|
||||
<path
|
||||
style="fill:#6d758a;fill-opacity:1;stroke:none;stroke-width:0.367058px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 88.161294,41.828827 C 62.047094,9.9170405 34.438258,-4.1510792 25.488049,1.0599515 16.95603,6.0275034 20.611925,42.54552 40.453804,77.723337 c 5.535677,9.814241 55.03746,-26.937222 47.70749,-35.89451 z"
|
||||
id="path447"
|
||||
sodipodi:nodetypes="ssss" />
|
||||
<path
|
||||
style="fill:#a0abc7;fill-opacity:1;stroke:none;stroke-width:0.176989px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 66.317189,37.01117 C 53.668122,21.693513 40.295092,14.940816 35.959835,17.44211 31.827139,19.826534 33.597962,37.355182 43.208873,54.240535 45.890217,58.951371 69.867642,41.310669 66.317189,37.01117 Z"
|
||||
id="path2506"
|
||||
sodipodi:nodetypes="ssss" />
|
||||
<circle
|
||||
style="fill:#141414;fill-opacity:1;stroke:none;stroke-width:0.338083;stroke-opacity:1"
|
||||
id="path234"
|
||||
cx="135.46666"
|
||||
cy="151.87083"
|
||||
r="119.0625" />
|
||||
<circle
|
||||
style="fill:#ffbf00;fill-opacity:1;stroke:none;stroke-width:0.22539;stroke-opacity:1"
|
||||
id="circle2604"
|
||||
cx="135.46666"
|
||||
cy="151.87083"
|
||||
r="79.375" />
|
||||
<circle
|
||||
style="fill:#141414;fill-opacity:1;stroke:none;stroke-width:0.112695;stroke-opacity:1"
|
||||
id="circle2602"
|
||||
cx="135.46666"
|
||||
cy="151.87083"
|
||||
r="39.6875" />
|
||||
<path
|
||||
style="fill:#a0abc7;fill-opacity:1;stroke:none;stroke-width:0.176989px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 204.61614,37.01117 c 12.64906,-15.317657 26.02209,-22.070354 30.35735,-19.56906 4.1327,2.384424 2.36187,19.913072 -7.24904,36.798425 -2.68134,4.710836 -26.65877,-12.929866 -23.10831,-17.229365 z"
|
||||
id="path295"
|
||||
sodipodi:nodetypes="ssss" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata344">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="" />
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
|
@ -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)
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,12 @@ 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"
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
self.sorting: list[Qt.SortOrder] | None = None
|
||||
|
@ -27,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")
|
||||
|
@ -49,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]
|
||||
|
||||
|
@ -60,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
|
||||
|
||||
|
@ -68,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
|
||||
|
@ -119,45 +174,43 @@ class Playlist:
|
|||
for track in self.tracks:
|
||||
wbz_data += f"{track.path}\n"
|
||||
|
||||
wbz = open(
|
||||
os.path.expanduser(
|
||||
f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
|
||||
),
|
||||
"w"
|
||||
)
|
||||
wbz = open(self.path, "w")
|
||||
wbz.write(wbz_data)
|
||||
wbz.close()
|
||||
|
||||
def rename(self, title: str):
|
||||
# remove from unique names so a new playlist can have the old name and delete old playlist.
|
||||
|
||||
path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
|
||||
path = os.path.expanduser(path)
|
||||
|
||||
if os.path.exists(path):
|
||||
os.remove(os.path.expanduser(path))
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
|
||||
old_title = self.title
|
||||
self.title = self.app.utils.unique_name(title, ignore=old_title)
|
||||
|
||||
self.path = os.path.expanduser(
|
||||
f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
|
||||
)
|
||||
|
||||
if not old_title == self.title: # remove only when the playlist actually has a different name
|
||||
self.app.utils.unique_names.remove(old_title)
|
||||
|
||||
def delete(self):
|
||||
path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
|
||||
path = os.path.expanduser(path)
|
||||
|
||||
if os.path.exists(path):
|
||||
os.remove(os.path.expanduser(path))
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
|
||||
self.app.utils.unique_names.remove(self.title)
|
||||
self.app.library.playlists.remove(self)
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,29 +1,20 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from pydub import AudioSegment
|
||||
from pydub.effects import normalize
|
||||
from pygame.mixer import Sound
|
||||
from tinytag import TinyTag
|
||||
|
||||
|
||||
SUPPORTED_FORMATS = [
|
||||
"mp3",
|
||||
"wav",
|
||||
"ogg"
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
@ -42,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
|
||||
|
||||
|
@ -56,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()
|
||||
|
@ -74,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):
|
||||
|
@ -83,11 +78,12 @@ class Track:
|
|||
self.sound = None
|
||||
self.duration = 0
|
||||
|
||||
def load_audio(self):
|
||||
file_type = self.path.split(".")[-1]
|
||||
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images
|
||||
|
||||
if file_type in SUPPORTED_FORMATS:
|
||||
self.audio = AudioSegment.from_file(self.path)
|
||||
def load_audio(self):
|
||||
#file_type = self.path.split(".")[-1]
|
||||
|
||||
self.audio = AudioSegment.from_file(self.path)
|
||||
|
||||
def remaining(self, position: int):
|
||||
remaining_audio = self.audio[position:]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtWidgets import QMainWindow, QMenu
|
||||
from .track_control import TrackControl
|
||||
from .settings import Settings
|
||||
from .process.process_dock import ProcessDock
|
||||
from .track_info import TrackInfo
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
@ -12,7 +15,10 @@ class MainWindow(QMainWindow):
|
|||
|
||||
self.app = app
|
||||
|
||||
self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg")
|
||||
|
||||
self.setWindowTitle("Wobuzz")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
self.menu_bar = self.menuBar()
|
||||
|
||||
|
@ -24,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)
|
||||
|
||||
|
@ -31,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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -36,6 +36,10 @@ class PlaylistTabBar(QTabBar):
|
|||
|
||||
def on_doubleclick(self, index):
|
||||
playlist_view = self.tab_widget.widget(index)
|
||||
|
||||
if playlist_view is None: # dont crash if no playlist was double-clicked
|
||||
return
|
||||
|
||||
playlist = playlist_view.playlist
|
||||
|
||||
self.app.player.start_playlist(playlist)
|
||||
|
|
|
@ -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):
|
||||
|
|
1
wobuzz/ui/process/__init__.py
Normal file
1
wobuzz/ui/process/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/python3
|
41
wobuzz/ui/process/process.py
Normal file
41
wobuzz/ui/process/process.py
Normal 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())
|
||||
|
75
wobuzz/ui/process/process_dock.py
Normal file
75
wobuzz/ui/process/process_dock.py
Normal 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()
|
||||
|
|
@ -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)
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
91
wobuzz/ui/track_info.py
Normal 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)
|
||||
|
||||
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue