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/settings.json
|
||||||
Wobuzz.egg-info
|
Wobuzz.egg-info
|
||||||
|
build
|
||||||
__pycache__
|
__pycache__
|
||||||
.idea
|
.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
|
### Features
|
||||||
|
|
||||||
| Feature | Description | State |
|
| 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 |
|
| 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 |
|
| 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
|
## Installation
|
||||||
|
|
||||||
To install Wobuzz, you firstly have to install the dependencies that can't be installed using pip.
|
### Release installation
|
||||||
This can be done using:
|
|
||||||
|
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
|
``` 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
|
``` bash
|
||||||
git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git
|
git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git
|
||||||
cd Wobuzz
|
cd Wobuzz
|
||||||
pip install .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage:
|
## Usage:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
PyQt6
|
PyQt6
|
||||||
pygame
|
pygame
|
||||||
tinytag
|
tinytag
|
||||||
pydub
|
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
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import setuptools
|
import setuptools
|
||||||
|
import os
|
||||||
from pathlib import Path
|
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
|
# use readme file as long description
|
||||||
this_directory = Path(__file__).parent
|
|
||||||
long_description = (this_directory / "README.md").read_text()
|
long_description = (this_directory / "README.md").read_text()
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="Wobuzz",
|
name="Wobuzz",
|
||||||
version="0.0",
|
version="0.1a2",
|
||||||
description="An audio player made by The Wobbler",
|
description="An audio player made by The Wobbler",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz",
|
url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz",
|
||||||
author="The Wobbler",
|
author="The Wobbler",
|
||||||
author_email="emil@i21k.de",
|
author_email="emil@i21k.de",
|
||||||
|
license="GNU GPLv3",
|
||||||
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
|
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
|
||||||
package_data={"": ["*.txt"]},
|
package_data={"": ["*.txt", "*.svg"]},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"PyQt6",
|
"PyQt6",
|
||||||
"tinytag",
|
"tinytag",
|
||||||
"pydub",
|
"pydub",
|
||||||
"pygame",
|
"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={
|
entry_points={
|
||||||
"console_scripts": ["wobuzz=wobuzz.command_line:main"],
|
"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 sys
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
from wobuzz.player.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
description = "A music player made by The Wobbler."
|
description = "A music player made by The Wobbler."
|
||||||
|
@ -20,23 +22,24 @@ def main():
|
||||||
app = Wobuzz()
|
app = Wobuzz()
|
||||||
|
|
||||||
if arguments.playlist:
|
if arguments.playlist:
|
||||||
app.library.temporary_playlist.clear()
|
playlist = Playlist(app, "Temporary Playlist", arguments.playlist)
|
||||||
app.library.temporary_playlist.view.clear()
|
|
||||||
app.library.temporary_playlist.load_from_m3u(arguments.playlist)
|
app.library.playlists.append(playlist)
|
||||||
app.library.temporary_playlist.view.load_tracks()
|
|
||||||
|
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:
|
if arguments.track:
|
||||||
app.library.temporary_playlist.clear()
|
playlist = Playlist(app, "Temporary Playlist", arguments.track)
|
||||||
app.library.temporary_playlist.view.clear()
|
|
||||||
|
|
||||||
# make track paths absolute
|
app.library.playlists.append(playlist)
|
||||||
tracks = []
|
|
||||||
|
|
||||||
for track in arguments.track:
|
if app.library.temporary_playlist in app.library.playlists:
|
||||||
tracks.append(os.path.abspath(track))
|
app.library.playlists.remove(app.library.temporary_playlist)
|
||||||
|
app.library.temporary_playlist = playlist
|
||||||
|
|
||||||
app.library.temporary_playlist.load_from_paths(tracks)
|
app.library.load_playlist_views()
|
||||||
app.library.temporary_playlist.view.load_tracks()
|
|
||||||
|
|
||||||
sys.exit(app.qt_app.exec())
|
sys.exit(app.qt_app.exec())
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@ class GUI:
|
||||||
|
|
||||||
self.dropped = []
|
self.dropped = []
|
||||||
|
|
||||||
self.clicked_playlist = self.app.library.temporary_playlist
|
|
||||||
|
|
||||||
self.window = MainWindow(app)
|
self.window = MainWindow(app)
|
||||||
self.settings = self.window.settings
|
self.settings = self.window.settings
|
||||||
self.track_control = self.window.track_control
|
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(
|
self.app.library.main_library_dock.setFeatures(
|
||||||
QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
||||||
|
@ -52,5 +52,18 @@ class GUI:
|
||||||
|
|
||||||
def on_track_change(self, previous_track, track):
|
def on_track_change(self, previous_track, track):
|
||||||
self.track_control.on_track_change(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
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from PyQt6.QtWidgets import QTabWidget
|
from PyQt6.QtWidgets import QTabWidget, QAbstractItemView
|
||||||
from ..player.playlist import Playlist
|
from ..player.playlist import Playlist
|
||||||
from ..ui.library_dock import LibraryDock
|
from ..ui.library_dock import LibraryDock
|
||||||
from ..ui.playlist import PlaylistView
|
from ..ui.playlist import PlaylistView
|
||||||
|
@ -18,10 +18,13 @@ class Library:
|
||||||
self.main_library_dock = LibraryDock(self)
|
self.main_library_dock = LibraryDock(self)
|
||||||
self.library_docks = [self.main_library_dock]
|
self.library_docks = [self.main_library_dock]
|
||||||
|
|
||||||
self.temporary_playlist = Playlist(self.app, "Temporary Playlist")
|
self.playlists = []
|
||||||
self.playlists = [self.temporary_playlist]
|
self.temporary_playlist = None
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
|
self.load_playlists()
|
||||||
|
|
||||||
|
def load_playlists(self):
|
||||||
path_playlists = os.path.expanduser(f"{self.app.settings.library_path}/playlists")
|
path_playlists = os.path.expanduser(f"{self.app.settings.library_path}/playlists")
|
||||||
|
|
||||||
if not os.path.exists(path_playlists):
|
if not os.path.exists(path_playlists):
|
||||||
|
@ -37,16 +40,11 @@ class Library:
|
||||||
if file_name.endswith(".m3u"):
|
if file_name.endswith(".m3u"):
|
||||||
path = f"{path_playlists}/{file_name}"
|
path = f"{path_playlists}/{file_name}"
|
||||||
|
|
||||||
if file_name == "Temporary_Playlist.wbz.m3u":
|
playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
|
||||||
playlist = self.temporary_playlist
|
self.playlists.append(playlist)
|
||||||
|
|
||||||
else:
|
if playlist.title == "Temporary Playlist":
|
||||||
playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
|
self.temporary_playlist = playlist
|
||||||
self.playlists.append(playlist)
|
|
||||||
|
|
||||||
playlist.load_from_m3u(path)
|
|
||||||
|
|
||||||
self.load_playlist_views()
|
|
||||||
|
|
||||||
def load_playlist_views(self):
|
def load_playlist_views(self):
|
||||||
for library_dock in self.library_docks:
|
for library_dock in self.library_docks:
|
||||||
|
@ -55,20 +53,35 @@ class Library:
|
||||||
playlist_tabs.playlists = {}
|
playlist_tabs.playlists = {}
|
||||||
|
|
||||||
for playlist in self.playlists:
|
for playlist in self.playlists:
|
||||||
playlist_view = PlaylistView(playlist)
|
playlist_view = PlaylistView(playlist, library_dock)
|
||||||
playlist_tabs.addTab(playlist_view, playlist.title)
|
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):
|
def on_exit(self, event):
|
||||||
for playlist in self.playlists:
|
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):
|
def new_playlist(self):
|
||||||
playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist"))
|
playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist"))
|
||||||
|
playlist.loaded = True
|
||||||
|
|
||||||
self.playlists.append(playlist)
|
self.playlists.append(playlist)
|
||||||
|
|
||||||
for library_dock in self.library_docks:
|
for library_dock in self.library_docks:
|
||||||
playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs
|
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)
|
playlist_tabs.addTab(playlist_view, playlist.title)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from wobbl_tools.data_file import load_dataclass_json
|
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 = load_dataclass_json(Settings, self.utils.settings_location, self, True, True)
|
||||||
self.settings.set_attribute_change_event(self.on_settings_change)
|
self.settings.set_attribute_change_event(self.on_settings_change)
|
||||||
|
|
||||||
self.player = Player(self)
|
|
||||||
self.library = Library(self)
|
self.library = Library(self)
|
||||||
|
self.player = Player(self)
|
||||||
self.gui = GUI(self)
|
self.gui = GUI(self)
|
||||||
|
|
||||||
self.post_init()
|
self.late_init()
|
||||||
|
|
||||||
def post_init(self):
|
def late_init(self):
|
||||||
self.gui.track_control.track_progress_slider.post_init()
|
self.gui.track_control.track_progress_slider.late_init()
|
||||||
self.library.load()
|
self.library.load()
|
||||||
|
|
||||||
def on_settings_change(self, key, value):
|
def on_settings_change(self, key, value):
|
||||||
|
@ -38,4 +37,7 @@ class Wobuzz:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
wobuzz = Wobuzz()
|
wobuzz = Wobuzz()
|
||||||
|
wobuzz.post_init()
|
||||||
|
wobuzz.library.load_playlist_views()
|
||||||
|
|
||||||
sys.exit(wobuzz.qt_app.exec())
|
sys.exit(wobuzz.qt_app.exec())
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import time
|
||||||
import threading
|
import threading
|
||||||
import pygame.mixer
|
import pygame.mixer
|
||||||
import pygame.event
|
import pygame.event
|
||||||
|
@ -18,7 +19,7 @@ class Player:
|
||||||
self.track_progress = TrackProgress(self.app)
|
self.track_progress = TrackProgress(self.app)
|
||||||
|
|
||||||
self.history = Playlist(self.app, "History")
|
self.history = Playlist(self.app, "History")
|
||||||
self.current_playlist = Playlist(self.app, "None")
|
self.current_playlist = None
|
||||||
|
|
||||||
self.playing = False
|
self.playing = False
|
||||||
self.paused = False
|
self.paused = False
|
||||||
|
@ -32,7 +33,7 @@ class Player:
|
||||||
self.playing = True
|
self.playing = True
|
||||||
self.paused = False
|
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
|
# cache next track so it immediately starts when the current track finishes
|
||||||
self.cache_next_track()
|
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.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):
|
def play_track_in_playlist(self, track_index):
|
||||||
self.stop()
|
self.stop()
|
||||||
|
@ -98,7 +99,7 @@ class Player:
|
||||||
self.track_progress.pause()
|
self.track_progress.pause()
|
||||||
self.paused = True
|
self.paused = True
|
||||||
|
|
||||||
self.app.gui.track_control.on_playstate_update()
|
self.app.gui.on_playstate_update()
|
||||||
|
|
||||||
def unpause(self):
|
def unpause(self):
|
||||||
self.music_channel.unpause()
|
self.music_channel.unpause()
|
||||||
|
@ -107,7 +108,7 @@ class Player:
|
||||||
self.playing = True
|
self.playing = True
|
||||||
self.paused = False
|
self.paused = False
|
||||||
|
|
||||||
self.app.gui.track_control.on_playstate_update()
|
self.app.gui.on_playstate_update()
|
||||||
|
|
||||||
def next_track(self):
|
def next_track(self):
|
||||||
if not self.current_playlist.on_last_track():
|
if not self.current_playlist.on_last_track():
|
||||||
|
@ -134,13 +135,13 @@ class Player:
|
||||||
self.music_channel.stop()
|
self.music_channel.stop()
|
||||||
self.track_progress.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.current_sound_duration = self.current_playlist.current_track.duration
|
||||||
|
|
||||||
self.playing = False
|
self.playing = False
|
||||||
self.paused = False
|
self.paused = False
|
||||||
|
|
||||||
self.app.gui.track_control.on_playstate_update()
|
self.app.gui.on_playstate_update()
|
||||||
|
|
||||||
def seek(self, position: int):
|
def seek(self, position: int):
|
||||||
self.music_channel.stop()
|
self.music_channel.stop()
|
||||||
|
@ -160,8 +161,15 @@ class Player:
|
||||||
track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1]
|
track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1]
|
||||||
|
|
||||||
if not track.cached:
|
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()
|
track.cache()
|
||||||
|
|
||||||
|
self.app.gui.on_background_job_stop("Loading Track")
|
||||||
|
|
||||||
def cache_next_track(self):
|
def cache_next_track(self):
|
||||||
# function that creates a thread which will cache the next track
|
# function that creates a thread which will cache the next track
|
||||||
caching_thread = threading.Thread(target=self.caching_thread_function)
|
caching_thread = threading.Thread(target=self.caching_thread_function)
|
||||||
|
@ -170,6 +178,15 @@ class Player:
|
||||||
def start_playlist(self, playlist):
|
def start_playlist(self, playlist):
|
||||||
self.stop()
|
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_sound, self.current_sound_duration = playlist.set_track(0) # first track
|
||||||
self.current_playlist = playlist
|
self.current_playlist = playlist
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtWidgets import QAbstractItemView
|
||||||
from .track import Track
|
from .track import Track
|
||||||
|
|
||||||
|
|
||||||
class Playlist:
|
class Playlist:
|
||||||
def __init__(self, app, title: str):
|
def __init__(self, app, title: str, load_from=None):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.title = title # playlist title
|
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,
|
# add to unique names so if the playlist is loaded from disk,
|
||||||
# no other playlist can be created using the same name
|
# no other playlist can be created using the same name
|
||||||
self.app.utils.unique_names.append(self.title)
|
self.app.utils.unique_names.append(self.title)
|
||||||
|
@ -18,7 +26,12 @@ class Playlist:
|
||||||
self.tracks: list[Track] = []
|
self.tracks: list[Track] = []
|
||||||
self.current_track_index = 0
|
self.current_track_index = 0
|
||||||
self.current_track: Track | None = None
|
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):
|
def clear(self):
|
||||||
self.sorting: list[Qt.SortOrder] | None = None
|
self.sorting: list[Qt.SortOrder] | None = None
|
||||||
|
@ -27,19 +40,49 @@ class Playlist:
|
||||||
self.current_track = None
|
self.current_track = None
|
||||||
|
|
||||||
def load_from_paths(self, paths):
|
def load_from_paths(self, paths):
|
||||||
|
num_tracks = len(paths)
|
||||||
|
|
||||||
i = 0
|
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]
|
path = paths[i]
|
||||||
|
|
||||||
if os.path.isfile(path):
|
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
|
i += 1
|
||||||
|
|
||||||
# set current track to the first track if there is no currently playing track
|
self.loaded = True
|
||||||
if self.current_track is None and self.has_tracks():
|
|
||||||
self.current_track = self.tracks[0]
|
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):
|
def load_from_m3u(self, path):
|
||||||
file = open(path, "r")
|
file = open(path, "r")
|
||||||
|
@ -49,9 +92,19 @@ class Playlist:
|
||||||
lines = m3u.split("\n") # m3u entries are separated by newlines
|
lines = m3u.split("\n") # m3u entries are separated by newlines
|
||||||
lines = lines[:-1] # remove last entry because it is just an empty string
|
lines = lines[:-1] # remove last entry because it is just an empty string
|
||||||
|
|
||||||
i = 0
|
|
||||||
num_lines = len(lines)
|
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:
|
while i < num_lines:
|
||||||
line = lines[i]
|
line = lines[i]
|
||||||
|
|
||||||
|
@ -60,7 +113,7 @@ class Playlist:
|
||||||
|
|
||||||
continue
|
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
|
i += 1
|
||||||
|
|
||||||
|
@ -68,10 +121,12 @@ class Playlist:
|
||||||
if self.current_track is None and self.has_tracks():
|
if self.current_track is None and self.has_tracks():
|
||||||
self.current_track = self.tracks[0]
|
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):
|
def load_from_wbz(self, path):
|
||||||
pass
|
self.load_from_m3u(path) # placeholder
|
||||||
|
|
||||||
def has_tracks(self):
|
def has_tracks(self):
|
||||||
return len(self.tracks) > 0
|
return len(self.tracks) > 0
|
||||||
|
@ -119,45 +174,43 @@ class Playlist:
|
||||||
for track in self.tracks:
|
for track in self.tracks:
|
||||||
wbz_data += f"{track.path}\n"
|
wbz_data += f"{track.path}\n"
|
||||||
|
|
||||||
wbz = open(
|
wbz = open(self.path, "w")
|
||||||
os.path.expanduser(
|
|
||||||
f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
|
|
||||||
),
|
|
||||||
"w"
|
|
||||||
)
|
|
||||||
wbz.write(wbz_data)
|
wbz.write(wbz_data)
|
||||||
wbz.close()
|
wbz.close()
|
||||||
|
|
||||||
def rename(self, title: str):
|
def rename(self, title: str):
|
||||||
# remove from unique names so a new playlist can have the old name and delete old playlist.
|
# 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"
|
if os.path.exists(self.path):
|
||||||
path = os.path.expanduser(path)
|
os.remove(self.path)
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
os.remove(os.path.expanduser(path))
|
|
||||||
|
|
||||||
old_title = self.title
|
old_title = self.title
|
||||||
self.title = self.app.utils.unique_name(title, ignore=old_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
|
if not old_title == self.title: # remove only when the playlist actually has a different name
|
||||||
self.app.utils.unique_names.remove(old_title)
|
self.app.utils.unique_names.remove(old_title)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
|
if os.path.exists(self.path):
|
||||||
path = os.path.expanduser(path)
|
os.remove(self.path)
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
os.remove(os.path.expanduser(path))
|
|
||||||
|
|
||||||
self.app.utils.unique_names.remove(self.title)
|
self.app.utils.unique_names.remove(self.title)
|
||||||
self.app.library.playlists.remove(self)
|
self.app.library.playlists.remove(self)
|
||||||
|
|
||||||
def append_track(self, track):
|
if self.app.player.current_playlist == self: # stop if this is the current playlist
|
||||||
self.tracks.append(track)
|
self.app.player.stop()
|
||||||
|
self.app.player.current_playlist = None
|
||||||
|
|
||||||
if self.view:
|
def append_track(self, track):
|
||||||
self.view.append_track(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):
|
def h_last_track(self):
|
||||||
# get last track in history (only gets used in player.history)
|
# get last track in history (only gets used in player.history)
|
||||||
|
|
|
@ -1,29 +1,20 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from pydub.effects import normalize
|
|
||||||
from pygame.mixer import Sound
|
from pygame.mixer import Sound
|
||||||
from tinytag import TinyTag
|
from tinytag import TinyTag
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_FORMATS = [
|
|
||||||
"mp3",
|
|
||||||
"wav",
|
|
||||||
"ogg"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Track:
|
class Track:
|
||||||
"""
|
"""
|
||||||
Class containing data for a track like file path, raw data...
|
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.app = app
|
||||||
self.path = path
|
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.cached = False
|
||||||
self.audio = None
|
self.audio = None
|
||||||
|
@ -42,6 +33,7 @@ class Track:
|
||||||
new_occurrences = {}
|
new_occurrences = {}
|
||||||
|
|
||||||
for item in self.items:
|
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 = new_occurrences.get(item.playlist, {})
|
||||||
playlist_occurrences[id(item)] = item.index
|
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 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:
|
if self.app.player.current_playlist is not None:
|
||||||
for item in self.items:
|
if self.app.player.current_playlist.current_track is self:
|
||||||
if (
|
for item in self.items:
|
||||||
item.playlist in self.occurrences and
|
if (
|
||||||
self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index
|
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)])
|
):
|
||||||
|
self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)])
|
||||||
|
|
||||||
def cache(self):
|
def cache(self):
|
||||||
self.load_audio()
|
self.load_audio()
|
||||||
|
@ -74,6 +67,8 @@ class Track:
|
||||||
|
|
||||||
self.duration = len(self.audio) # track duration in milliseconds
|
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
|
self.cached = True
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
|
@ -83,11 +78,12 @@ class Track:
|
||||||
self.sound = None
|
self.sound = None
|
||||||
self.duration = 0
|
self.duration = 0
|
||||||
|
|
||||||
def load_audio(self):
|
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images
|
||||||
file_type = self.path.split(".")[-1]
|
|
||||||
|
|
||||||
if file_type in SUPPORTED_FORMATS:
|
def load_audio(self):
|
||||||
self.audio = AudioSegment.from_file(self.path)
|
#file_type = self.path.split(".")[-1]
|
||||||
|
|
||||||
|
self.audio = AudioSegment.from_file(self.path)
|
||||||
|
|
||||||
def remaining(self, position: int):
|
def remaining(self, position: int):
|
||||||
remaining_audio = self.audio[position:]
|
remaining_audio = self.audio[position:]
|
||||||
|
|
|
@ -30,5 +30,5 @@ class TrackProgress:
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.timer.stop()
|
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
|
self.remaining_time = self.app.player.current_playlist.current_track.duration
|
||||||
|
|
|
@ -9,4 +9,6 @@ class Settings:
|
||||||
window_maximized: bool=False
|
window_maximized: bool=False
|
||||||
library_path: str="~/.wobuzz"
|
library_path: str="~/.wobuzz"
|
||||||
clear_track_cache: bool=True
|
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.library = library
|
||||||
|
|
||||||
self.setAllowedAreas(
|
|
||||||
Qt.DockWidgetArea.LeftDockWidgetArea |
|
|
||||||
Qt.DockWidgetArea.RightDockWidgetArea |
|
|
||||||
Qt.DockWidgetArea.BottomDockWidgetArea
|
|
||||||
)
|
|
||||||
|
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
self.library_widget = Library(library, self)
|
self.library_widget = Library(library, self)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QIcon
|
||||||
from PyQt6.QtWidgets import QMainWindow, QMenu
|
from PyQt6.QtWidgets import QMainWindow, QMenu
|
||||||
from .track_control import TrackControl
|
from .track_control import TrackControl
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
|
from .process.process_dock import ProcessDock
|
||||||
|
from .track_info import TrackInfo
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
@ -12,7 +15,10 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
|
self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg")
|
||||||
|
|
||||||
self.setWindowTitle("Wobuzz")
|
self.setWindowTitle("Wobuzz")
|
||||||
|
self.setWindowIcon(self.icon)
|
||||||
|
|
||||||
self.menu_bar = self.menuBar()
|
self.menu_bar = self.menuBar()
|
||||||
|
|
||||||
|
@ -24,6 +30,11 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
self.settings_action = self.edit_menu.addAction("&Settings")
|
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.track_control = TrackControl(app)
|
||||||
self.addToolBar(self.track_control)
|
self.addToolBar(self.track_control)
|
||||||
|
|
||||||
|
@ -31,5 +42,13 @@ class MainWindow(QMainWindow):
|
||||||
self.settings.hide()
|
self.settings.hide()
|
||||||
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.settings)
|
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.QtCore import pyqtSignal
|
||||||
from PyQt6.QtGui import QDropEvent, QIcon, QFont
|
from PyQt6.QtGui import QDropEvent, QIcon, QFont
|
||||||
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame
|
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
|
||||||
|
|
||||||
from .track import TrackItem
|
from .track import TrackItem
|
||||||
|
|
||||||
|
@ -10,25 +10,22 @@ from .track import TrackItem
|
||||||
class PlaylistView(QTreeWidget):
|
class PlaylistView(QTreeWidget):
|
||||||
itemDropped = pyqtSignal(QTreeWidget, list)
|
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)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.playlist = playlist
|
self.playlist = playlist
|
||||||
|
self.library_dock = dock
|
||||||
|
|
||||||
self.app = playlist.app
|
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.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
|
||||||
self.setColumnCount(4)
|
self.setColumnCount(4)
|
||||||
|
|
||||||
self.playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
|
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
"#",
|
"#",
|
||||||
"Title",
|
"Title",
|
||||||
|
@ -39,8 +36,6 @@ class PlaylistView(QTreeWidget):
|
||||||
|
|
||||||
self.setHeaderLabels(headers)
|
self.setHeaderLabels(headers)
|
||||||
|
|
||||||
self.load_tracks()
|
|
||||||
|
|
||||||
self.itemActivated.connect(self.on_track_activation)
|
self.itemActivated.connect(self.on_track_activation)
|
||||||
|
|
||||||
def on_user_sort(self):
|
def on_user_sort(self):
|
||||||
|
@ -63,7 +58,8 @@ class PlaylistView(QTreeWidget):
|
||||||
|
|
||||||
i += 1
|
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()
|
self.app.player.cache_next_track()
|
||||||
|
|
||||||
def dropEvent(self, event: QDropEvent):
|
def dropEvent(self, event: QDropEvent):
|
||||||
|
@ -133,20 +129,14 @@ class PlaylistView(QTreeWidget):
|
||||||
|
|
||||||
# unmark the previous track in all playlists
|
# unmark the previous track in all playlists
|
||||||
for item in previous_track.items:
|
for item in previous_track.items:
|
||||||
item.setIcon(0, QIcon(None))
|
item.unmark()
|
||||||
item.setFont(1, self.normal_font)
|
|
||||||
item.setFont(2, self.normal_font)
|
|
||||||
item.setFont(3, self.normal_font)
|
|
||||||
|
|
||||||
if track:
|
if track:
|
||||||
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
|
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
|
||||||
|
|
||||||
# mark the current track in this playlist
|
# mark the current track in this playlist
|
||||||
item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
|
item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
|
||||||
item.setIcon(0, self.playing_mark)
|
item.mark()
|
||||||
item.setFont(1, self.bold_font)
|
|
||||||
item.setFont(2, self.bold_font)
|
|
||||||
item.setFont(3, self.normal_font)
|
|
||||||
|
|
||||||
def append_track(self, track):
|
def append_track(self, track):
|
||||||
TrackItem(track, self.topLevelItemCount() - 1, self)
|
TrackItem(track, self.topLevelItemCount() - 1, self)
|
||||||
|
|
|
@ -36,6 +36,10 @@ class PlaylistTabBar(QTabBar):
|
||||||
|
|
||||||
def on_doubleclick(self, index):
|
def on_doubleclick(self, index):
|
||||||
playlist_view = self.tab_widget.widget(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
|
playlist = playlist_view.playlist
|
||||||
|
|
||||||
self.app.player.start_playlist(playlist)
|
self.app.player.start_playlist(playlist)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QMouseEvent
|
from PyQt6.QtGui import QMouseEvent, QCursor
|
||||||
from PyQt6.QtWidgets import QLineEdit
|
from PyQt6.QtWidgets import QLineEdit
|
||||||
|
|
||||||
from .tab_bar import PlaylistTabBar
|
from .tab_bar import PlaylistTabBar
|
||||||
|
@ -16,10 +16,11 @@ class TabTitle(QLineEdit):
|
||||||
self.index = index
|
self.index = index
|
||||||
self.playlist_view = playlist_view
|
self.playlist_view = playlist_view
|
||||||
|
|
||||||
self.setStyleSheet("QLineEdit {background: transparent;}")
|
self.setStyleSheet("QLineEdit {background: transparent; border: none;}")
|
||||||
|
|
||||||
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
|
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
|
||||||
|
|
||||||
|
self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor)
|
||||||
|
|
||||||
self.editingFinished.connect(self.on_edit)
|
self.editingFinished.connect(self.on_edit)
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
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.layout = QFormLayout(self)
|
||||||
self.setLayout(self.layout)
|
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.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)
|
||||||
|
|
|
@ -44,6 +44,7 @@ class Settings(QDockWidget):
|
||||||
def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event
|
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.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.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):
|
def update_settings(self, key, value):
|
||||||
match key:
|
match key:
|
||||||
|
@ -53,7 +54,11 @@ class Settings(QDockWidget):
|
||||||
case "clear_track_cache":
|
case "clear_track_cache":
|
||||||
self.behavior_settings.clear_track_cache.setDown(value)
|
self.behavior_settings.clear_track_cache.setDown(value)
|
||||||
|
|
||||||
|
case "load_on_start":
|
||||||
|
self.behavior_settings.load_on_start.setChecked(value)
|
||||||
|
|
||||||
def write_settings(self):
|
def write_settings(self):
|
||||||
self.app.settings.library_path = self.file_settings.library_path_input.text()
|
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.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
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QFont, QIcon, QPalette
|
||||||
from PyQt6.QtWidgets import QTreeWidgetItem
|
from PyQt6.QtWidgets import QTreeWidgetItem
|
||||||
|
|
||||||
|
|
||||||
class TrackItem(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):
|
def __init__(self, track, index, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.track = track
|
self.track = track
|
||||||
self.index_user_sort = index
|
self.index_user_sort = index
|
||||||
|
|
||||||
self.index = index
|
self.index = index
|
||||||
|
|
||||||
self.playlist = parent.playlist
|
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.items.append(self)
|
||||||
|
|
||||||
track.set_occurrences()
|
track.set_occurrences()
|
||||||
|
@ -31,3 +42,16 @@ class TrackItem(QTreeWidgetItem):
|
||||||
self.setText(3, track.tags.album)
|
self.setText(3, track.tags.album)
|
||||||
self.setText(4, str(self.index_user_sort + 1))
|
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)
|
self.next_button.triggered.connect(self.next_track)
|
||||||
|
|
||||||
def previous_track(self):
|
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()
|
self.app.player.previous_track()
|
||||||
|
|
||||||
def stop(self):
|
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()
|
self.app.player.stop()
|
||||||
|
|
||||||
def next_track(self):
|
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()
|
self.app.player.next_track()
|
||||||
|
|
||||||
def on_track_change(self, previous_track, track):
|
def on_track_change(self, previous_track, track):
|
||||||
|
@ -80,11 +80,17 @@ class TrackControl(QToolBar):
|
||||||
elif self.app.player.playing: # playing
|
elif self.app.player.playing: # playing
|
||||||
self.app.player.pause()
|
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()
|
self.app.player.start_playing()
|
||||||
|
|
||||||
elif self.app.player.current_playlist.title == "None":
|
elif self.app.player.current_playlist is None:
|
||||||
self.app.player.start_playlist(self.app.gui.clicked_playlist)
|
if self.app.settings.latest_playlist is not None:
|
||||||
|
for playlist in self.app.library.playlists: # 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):
|
def on_playstate_update(self):
|
||||||
if self.app.player.playing:
|
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.sliderPressed.connect(self.on_press)
|
||||||
self.sliderReleased.connect(self.on_release)
|
self.sliderReleased.connect(self.on_release)
|
||||||
|
|
||||||
def post_init(self):
|
def late_init(self):
|
||||||
self.track_control = self.app.gui.track_control
|
self.track_control = self.app.gui.track_control
|
||||||
|
|
||||||
def mousePressEvent(self, event: QMouseEvent):
|
def mousePressEvent(self, event: QMouseEvent):
|
||||||
|
@ -57,7 +57,7 @@ class TrackProgressSlider(QSlider):
|
||||||
def on_release(self):
|
def on_release(self):
|
||||||
self.dragged = False
|
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())
|
self.app.player.seek(self.value())
|
||||||
|
|
||||||
def update_progress(self):
|
def update_progress(self):
|
||||||
|
|
Loading…
Add table
Reference in a new issue