diff --git a/wobuzz/library/__init__.py b/wobuzz/library/__init__.py index a93a4bf..8fc6dc1 100644 --- a/wobuzz/library/__init__.py +++ b/wobuzz/library/__init__.py @@ -1 +1,8 @@ #!/usr/bin/python3 + +def __getattr__(name): + match name: + case "Library": + from .library import Library + + return Library diff --git a/wobuzz/library/library.py b/wobuzz/library/library.py index a59b1f7..56f88f8 100644 --- a/wobuzz/library/library.py +++ b/wobuzz/library/library.py @@ -1,12 +1,12 @@ #!/usr/bin/python3 import os -import shutil from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from ..player.playlist import Playlist from ..ui.library.library import LibraryWidget from ..ui.playlist_view import PlaylistView +from ..types import Types class Library: @@ -115,15 +115,28 @@ class Library: playlist.load() - def import_tracks(self, tracks: list[str]): - playlist = Playlist(self.app, "Temporary Playlist", tracks, True) + def import_tracks( + self, + tracks: list[str], + import_options: Types.ImportOptions + ): + playlist = Playlist(self.app, "Temporary Playlist", tracks, import_options) self.replace_temporary_playlist(playlist) self.load_playlist_views() playlist.load() - def import_track(self, track): + def import_track(self, track, import_options: Types.ImportOptions): + if import_options.artist is not None: + track.metadata.artist = import_options.artist + + if import_options.album is not None: + track.metadata.album = import_options.album + + if import_options.genre is not None: + track.metadata.genre = import_options.genre + artist_path = os.path.expanduser(f"{self.app.settings.library_path}/artists/{track.metadata.artist}") if not os.path.exists(artist_path): @@ -134,10 +147,7 @@ class Library: if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library return - shutil.copyfile(track.path, new_track_path) - - track.path = new_track_path - track.metadata.path = new_track_path + track.copy(new_track_path, import_options.copy_type) def open_playlist(self, playlist_path: str): playlist = Playlist(self.app, "Temporary Playlist", playlist_path) @@ -148,8 +158,8 @@ class Library: playlist.load() - def import_playlist(self, playlist_path: str): - playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_tracks=True) + def import_playlist(self, playlist_path: str, import_options): + playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_options) self.replace_temporary_playlist(playlist) diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index fdc0580..0d5fd2e 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -2,16 +2,16 @@ import os import threading - from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QAbstractItemView from .track import Track, TrackMetadata from ..wobuzzm3u import WobuzzM3U, WBZM3UData +from ..types import Types class Playlist: - def __init__(self, app, title: str, load_from=None, import_tracks: bool=False): + def __init__(self, app, title: str, load_from=None, import_options: Types.ImportOptions=None): self.app = app self.title = title # playlist title @@ -20,7 +20,7 @@ class Playlist: # if None, playlist should be already in the library and will be loaded from a .wbz.m3u self.load_from = load_from - self.import_tracks = import_tracks + self.import_options = import_options # add to unique names so if the playlist is loaded from disk, # no other playlist can be created using the same name @@ -96,9 +96,9 @@ class Playlist: self.loading = False - if self.import_tracks: + if self.import_options is not None: for track in self.tracks: - self.app.library.import_track(track) + self.app.library.import_track(track, self.import_options) for dock_id in self.views: # enable drag and drop on every view view = self.views[dock_id] diff --git a/wobuzz/player/track.py b/wobuzz/player/track.py index face0dd..faf0f8b 100644 --- a/wobuzz/player/track.py +++ b/wobuzz/player/track.py @@ -1,11 +1,15 @@ #!/usr/bin/python3 +import os +import shutil from pydub import AudioSegment from pygame.mixer import Sound from tinytag import TinyTag from tinytag.tinytag import Images as TTImages from dataclasses import dataclass +from ..types import Types + @dataclass class TrackMetadata: @@ -163,3 +167,18 @@ class Track: self.items.remove(item) self.occurrences.pop(playlist) + + def copy(self, dest: str, copy_type: int=Types.CopyType.symlink, moved: bool=True): + match copy_type: + case Types.CopyType.symlink: + os.symlink(self.path, dest) + + case Types.CopyType.copy: + shutil.copyfile(self.path, dest) + + case Types.CopyType.move: + shutil.move(self.path, dest) + + if moved: # update path variables + self.path = dest + self.metadata.path = dest diff --git a/wobuzz/types/__init__.py b/wobuzz/types/__init__.py new file mode 100644 index 0000000..052adfa --- /dev/null +++ b/wobuzz/types/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/python3 + +def __getattr__(name): + match name: + case "Types": + from .types import Types + return Types + + case "ImportOptions": + from .import_options import ImportOptions + return ImportOptions + + case "CopyType": + from .import_options import CopyType + return CopyType diff --git a/wobuzz/types/import_options.py b/wobuzz/types/import_options.py new file mode 100644 index 0000000..ceed274 --- /dev/null +++ b/wobuzz/types/import_options.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +from dataclasses import dataclass + + +@dataclass +class ImportOptions: + artist: str=None + album: str=None + genre: str=None + + copy_type=0 + + +class CopyType: + symlink = 0 + copy = 1 + move = 2 diff --git a/wobuzz/types/types.py b/wobuzz/types/types.py new file mode 100644 index 0000000..dbbab54 --- /dev/null +++ b/wobuzz/types/types.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +from . import ImportOptions +from . import CopyType + + +class Types: + ImportOptions = ImportOptions + CopyType = CopyType diff --git a/wobuzz/ui/custom_widgets/__init__.py b/wobuzz/ui/custom_widgets/__init__.py new file mode 100644 index 0000000..6cfdb5a --- /dev/null +++ b/wobuzz/ui/custom_widgets/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python3 + + +def __getattr__(name): + match name: + case "GroupBox": + from .group_box import GroupBox + return GroupBox diff --git a/wobuzz/ui/custom_widgets/group_box.py b/wobuzz/ui/custom_widgets/group_box.py new file mode 100644 index 0000000..8b61620 --- /dev/null +++ b/wobuzz/ui/custom_widgets/group_box.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QGroupBox, QSizePolicy + + +class GroupBox(QGroupBox): + """ + Just a QGroupBox with some custom style I don't always want to rewrite. + """ + + def __init__(self, title, parent=None): + super().__init__(title, parent) + + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) + + self.setStyleSheet("QGroupBox{font-weight: bold;}") diff --git a/wobuzz/ui/library/import_dialog.py b/wobuzz/ui/library/import_dialog.py new file mode 100644 index 0000000..e37bfbe --- /dev/null +++ b/wobuzz/ui/library/import_dialog.py @@ -0,0 +1,116 @@ +#!/usr/bin/python3 + +from PyQt6.QtWidgets import ( + QWidget, + QLabel, + QDialog, + QCheckBox, + QLineEdit, + QDialogButtonBox, + QButtonGroup, + QRadioButton, + QVBoxLayout, + QHBoxLayout, + QFormLayout, + QSizePolicy, +) +from PyQt6.QtCore import Qt + +from ..custom_widgets import GroupBox + + +class ImportDialog(QDialog): + def __init__(self): + super().__init__() + + self.setWindowTitle("Import") + + layout = QVBoxLayout(self) + self.setLayout(layout) + + self.tagging_section = GroupBox("Metadata And Tagging", self) + self.tagging_section.layout = QFormLayout(self.tagging_section) + self.tagging_section.setLayout(self.tagging_section.layout) + layout.addWidget(self.tagging_section) + + self.tagging_section.overwrite_metadata = QCheckBox( + "Set custom metadata for all tracks (Leave property blank to keep the metadata from the audio file.)", + self.tagging_section + ) + self.tagging_section.layout.addRow(self.tagging_section.overwrite_metadata) + + self.tagging_section.artist = QLineEdit(self.tagging_section) + self.tagging_section.layout.addRow(" Artist: ", self.tagging_section.artist) + self.tagging_section.artist.setPlaceholderText("Keep track artist") + + self.tagging_section.album = QLineEdit(self.tagging_section) + self.tagging_section.layout.addRow(" Album: ", self.tagging_section.album) + self.tagging_section.album.setPlaceholderText("Keep track album") + + self.tagging_section.genre = QLineEdit(self.tagging_section) + self.tagging_section.layout.addRow(" Genre: ", self.tagging_section.genre) + self.tagging_section.genre.setPlaceholderText("Keep track genre") + + self.file_section = GroupBox("File Structure", self) + self.file_section.layout = QFormLayout(self.file_section) + self.file_section.setLayout(self.file_section.layout) + layout.addWidget(self.file_section) + + self.file_section.copy_type_description = QLabel("How should the tracks get put into the Wobuzz library?") + self.file_section.layout.addRow(self.file_section.copy_type_description) + + self.file_section.copy_type = QButtonGroup(self.file_section) + + self.file_section.copy_type_symlink = QRadioButton("Create symlinks", self.file_section) + self.file_section.copy_type_symlink.setChecked(True) + self.file_section.copy_type.addButton(self.file_section.copy_type_symlink) + self.file_section.layout.addWidget(self.file_section.copy_type_symlink) + + self.file_section.copy_type_copy = QRadioButton("Copy tracks", self.file_section) + self.file_section.copy_type.addButton(self.file_section.copy_type_copy) + self.file_section.layout.addWidget(self.file_section.copy_type_copy) + + self.file_section.copy_type_move = QRadioButton("Move tracks", self.file_section) + self.file_section.copy_type.addButton(self.file_section.copy_type_move) + self.file_section.layout.addWidget(self.file_section.copy_type_move) + + # add expanding widget so the GroupBoxes aren't vertically centered + spacer_widget = QWidget(self) + spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + layout.addWidget(spacer_widget) + + dialog_buttons = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + + self.dialog_buttons = QDialogButtonBox(dialog_buttons) + layout.addWidget(self.dialog_buttons) + + self.reset_inputs() + + self.tagging_section.overwrite_metadata.stateChanged.connect(self.overwrite_state_changed) + self.dialog_buttons.accepted.connect(self.accept) + self.dialog_buttons.rejected.connect(self.reject) + + def exec(self): + self.reset_inputs() + + return super().exec() + + def overwrite_state_changed(self, state: int): + overwrite = state == 2 + + self.tagging_section.artist.setEnabled(overwrite) + self.tagging_section.album.setEnabled(overwrite) + self.tagging_section.genre.setEnabled(overwrite) + + self.tagging_section.layout.setRowVisible(self.tagging_section.artist, overwrite) + self.tagging_section.layout.setRowVisible(self.tagging_section.album, overwrite) + self.tagging_section.layout.setRowVisible(self.tagging_section.genre, overwrite) + + def reset_inputs(self): + self.overwrite_state_changed(0) + + self.tagging_section.artist.setText("") + self.tagging_section.album.setText("") + self.tagging_section.genre.setText("") + + self.file_section.copy_type_symlink.setChecked(True) diff --git a/wobuzz/ui/popups.py b/wobuzz/ui/popups.py index 324e53f..7b7ec13 100644 --- a/wobuzz/ui/popups.py +++ b/wobuzz/ui/popups.py @@ -1,6 +1,9 @@ #!/usr/bin/python3 -from PyQt6.QtWidgets import QFileDialog +from PyQt6.QtWidgets import QDialog, QFileDialog + +from .library.import_dialog import ImportDialog +from ..types import Types class Popups: @@ -12,7 +15,7 @@ class Popups: self.audio_file_selector = QFileDialog(self.window, "Select Audio Files") self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) - self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus)", "Any (*)"]) + self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus *.m4a)", "Any (*)"]) self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List) self.playlist_file_selector = QFileDialog(self.window, "Select Playlist") @@ -20,6 +23,8 @@ class Popups: self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"]) self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) + self.import_dialog = ImportDialog() + self.window.open_track_action.triggered.connect(self.open_tracks) self.window.import_track_action.triggered.connect(self.import_tracks) self.window.open_playlist_action.triggered.connect(self.open_playlist) @@ -39,11 +44,43 @@ class Popups: if files is not None and not files == []: self.app.library.open_tracks(files) + def get_import_options(self): + import_options = Types.ImportOptions() + + if self.import_dialog.tagging_section.overwrite_metadata.isChecked(): + artist = self.import_dialog.tagging_section.artist.text() + album = self.import_dialog.tagging_section.album.text() + genre = self.import_dialog.tagging_section.genre.text() + + if not artist == "": + import_options.artist = artist + + if not album == "": + import_options.album = album + + if not genre == "": + import_options.genre = genre + + if self.import_dialog.file_section.copy_type_copy.isChecked(): + import_options.copy_type = Types.CopyType.copy + + elif self.import_dialog.file_section.copy_type_move.isChecked(): + import_options.copy_type = Types.CopyType.move + + return import_options + def import_tracks(self): files = self.select_audio_files() - if files is not None and not files == []: - self.app.library.import_tracks(files) + if files is None or files == []: + return + + if self.import_dialog.exec() == QDialog.rejected: + return + + import_options = self.get_import_options() + + self.app.library.import_tracks(files, import_options) def open_playlist(self): playlist_path = self.select_playlist_file() @@ -54,5 +91,12 @@ class Popups: def import_playlist(self): playlist_path = self.select_playlist_file() - if playlist_path is not None and not playlist_path == "": - self.app.library.import_playlist(playlist_path) + if playlist_path is None or playlist_path == "": + return + + if self.import_dialog.exec() == QDialog.rejected: + return + + import_options = self.get_import_options() + + self.app.library.import_playlist(playlist_path, import_options) diff --git a/wobuzz/ui/settings/sub_category.py b/wobuzz/ui/settings/sub_category.py index 694b682..6453b78 100644 --- a/wobuzz/ui/settings/sub_category.py +++ b/wobuzz/ui/settings/sub_category.py @@ -2,21 +2,18 @@ from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont -from PyQt6.QtWidgets import QGroupBox, QLabel, QSizePolicy, QFormLayout +from PyQt6.QtWidgets import QLabel, QSizePolicy, QFormLayout + +from ..custom_widgets import GroupBox -class SubCategory(QGroupBox): +class SubCategory(GroupBox): description_font = QFont() description_font.setPointSize(8) def __init__(self, title: str, description: str=None, parent=None): super().__init__(title, parent) - self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) - self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) - - self.setStyleSheet("QGroupBox{font-weight: bold;}") - self.layout = QFormLayout() self.setLayout(self.layout)