Added popup on import where the user can configure how the tracks get imported.

This commit is contained in:
The Wobbler 2025-03-08 17:50:45 +01:00
parent 31b2e3bf41
commit fd34476d00
12 changed files with 289 additions and 28 deletions

View file

@ -1 +1,8 @@
#!/usr/bin/python3 #!/usr/bin/python3
def __getattr__(name):
match name:
case "Library":
from .library import Library
return Library

View file

@ -1,12 +1,12 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import shutil
from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from PyQt6.QtWidgets import QTabWidget, QAbstractItemView
from ..player.playlist import Playlist from ..player.playlist import Playlist
from ..ui.library.library import LibraryWidget from ..ui.library.library import LibraryWidget
from ..ui.playlist_view import PlaylistView from ..ui.playlist_view import PlaylistView
from ..types import Types
class Library: class Library:
@ -115,15 +115,28 @@ class Library:
playlist.load() playlist.load()
def import_tracks(self, tracks: list[str]): def import_tracks(
playlist = Playlist(self.app, "Temporary Playlist", tracks, True) self,
tracks: list[str],
import_options: Types.ImportOptions
):
playlist = Playlist(self.app, "Temporary Playlist", tracks, import_options)
self.replace_temporary_playlist(playlist) self.replace_temporary_playlist(playlist)
self.load_playlist_views() self.load_playlist_views()
playlist.load() 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}") artist_path = os.path.expanduser(f"{self.app.settings.library_path}/artists/{track.metadata.artist}")
if not os.path.exists(artist_path): 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 if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library
return return
shutil.copyfile(track.path, new_track_path) track.copy(new_track_path, import_options.copy_type)
track.path = new_track_path
track.metadata.path = new_track_path
def open_playlist(self, playlist_path: str): def open_playlist(self, playlist_path: str):
playlist = Playlist(self.app, "Temporary Playlist", playlist_path) playlist = Playlist(self.app, "Temporary Playlist", playlist_path)
@ -148,8 +158,8 @@ class Library:
playlist.load() playlist.load()
def import_playlist(self, playlist_path: str): def import_playlist(self, playlist_path: str, import_options):
playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_tracks=True) playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_options)
self.replace_temporary_playlist(playlist) self.replace_temporary_playlist(playlist)

View file

@ -2,16 +2,16 @@
import os import os
import threading import threading
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QAbstractItemView from PyQt6.QtWidgets import QAbstractItemView
from .track import Track, TrackMetadata from .track import Track, TrackMetadata
from ..wobuzzm3u import WobuzzM3U, WBZM3UData from ..wobuzzm3u import WobuzzM3U, WBZM3UData
from ..types import Types
class Playlist: 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.app = app
self.title = title # playlist title 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 # if None, playlist should be already in the library and will be loaded from a .wbz.m3u
self.load_from = load_from 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, # 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
@ -96,9 +96,9 @@ class Playlist:
self.loading = False self.loading = False
if self.import_tracks: if self.import_options is not None:
for track in self.tracks: 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 for dock_id in self.views: # enable drag and drop on every view
view = self.views[dock_id] view = self.views[dock_id]

View file

@ -1,11 +1,15 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os
import shutil
from pydub import AudioSegment from pydub import AudioSegment
from pygame.mixer import Sound from pygame.mixer import Sound
from tinytag import TinyTag from tinytag import TinyTag
from tinytag.tinytag import Images as TTImages from tinytag.tinytag import Images as TTImages
from dataclasses import dataclass from dataclasses import dataclass
from ..types import Types
@dataclass @dataclass
class TrackMetadata: class TrackMetadata:
@ -163,3 +167,18 @@ class Track:
self.items.remove(item) self.items.remove(item)
self.occurrences.pop(playlist) 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

15
wobuzz/types/__init__.py Normal file
View file

@ -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

View file

@ -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

9
wobuzz/types/types.py Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/python3
from . import ImportOptions
from . import CopyType
class Types:
ImportOptions = ImportOptions
CopyType = CopyType

View file

@ -0,0 +1,8 @@
#!/usr/bin/python3
def __getattr__(name):
match name:
case "GroupBox":
from .group_box import GroupBox
return GroupBox

View file

@ -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;}")

View file

@ -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)

View file

@ -1,6 +1,9 @@
#!/usr/bin/python3 #!/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: class Popups:
@ -12,7 +15,7 @@ class Popups:
self.audio_file_selector = QFileDialog(self.window, "Select Audio Files") self.audio_file_selector = QFileDialog(self.window, "Select Audio Files")
self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles) 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.audio_file_selector.setViewMode(QFileDialog.ViewMode.List)
self.playlist_file_selector = QFileDialog(self.window, "Select Playlist") 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.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"])
self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List) self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List)
self.import_dialog = ImportDialog()
self.window.open_track_action.triggered.connect(self.open_tracks) self.window.open_track_action.triggered.connect(self.open_tracks)
self.window.import_track_action.triggered.connect(self.import_tracks) self.window.import_track_action.triggered.connect(self.import_tracks)
self.window.open_playlist_action.triggered.connect(self.open_playlist) self.window.open_playlist_action.triggered.connect(self.open_playlist)
@ -39,11 +44,43 @@ class Popups:
if files is not None and not files == []: if files is not None and not files == []:
self.app.library.open_tracks(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): def import_tracks(self):
files = self.select_audio_files() files = self.select_audio_files()
if files is not None and not files == []: if files is None or files == []:
self.app.library.import_tracks(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): def open_playlist(self):
playlist_path = self.select_playlist_file() playlist_path = self.select_playlist_file()
@ -54,5 +91,12 @@ class Popups:
def import_playlist(self): def import_playlist(self):
playlist_path = self.select_playlist_file() playlist_path = self.select_playlist_file()
if playlist_path is not None and not playlist_path == "": if playlist_path is None or playlist_path == "":
self.app.library.import_playlist(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)

View file

@ -2,21 +2,18 @@
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont 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 = QFont()
description_font.setPointSize(8) description_font.setPointSize(8)
def __init__(self, title: str, description: str=None, parent=None): def __init__(self, title: str, description: str=None, parent=None):
super().__init__(title, parent) 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.layout = QFormLayout()
self.setLayout(self.layout) self.setLayout(self.layout)