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
def __getattr__(name):
match name:
case "Library":
from .library import Library
return Library

View file

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

View file

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

View file

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

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

View file

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