Skip to content

Commit

Permalink
(closes #40) delete duplicates full UI
Browse files Browse the repository at this point in the history
  • Loading branch information
apastel committed Jun 22, 2024
1 parent fde97e1 commit 98085cb
Show file tree
Hide file tree
Showing 26 changed files with 1,159 additions and 96 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ If this project helped you and you want to thank me, you can <a href="https://ww

Features
--------
### Deleting
* Remove all songs and podcasts from your Library
* Delete all of your Uploads
* Option to automatically add the corresponding album to your library from within YT Music
* Delete all of your playlists
* Reset all of your "Liked" ratings
* Delete your play history
### Playlist Utilities
* Sort your playlists
* Remove duplicates from your playlists

![YTMusic Deleter screenshot](https://i.imgur.com/ZmGl58E.gif)
![YTMusic Deleter screenshot](https://i.imgur.com/TVpB6xY.gif)

## Installation
### Easy Install (Windows or Debian Linux Only)
Expand Down
130 changes: 130 additions & 0 deletions gui/src/main/python/checkbox_track_listing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from typing import Dict
from typing import List

import requests
from generated.ui_checkbox_track_listing import Ui_CheckboxTrackListingDialog
from PySide6.QtCore import Property
from PySide6.QtCore import QRect
from PySide6.QtCore import QSize
from PySide6.QtCore import Qt
from PySide6.QtCore import Signal
from PySide6.QtCore import Slot
from PySide6.QtGui import QBrush
from PySide6.QtGui import QColorConstants
from PySide6.QtGui import QImage
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QDialog
from PySide6.QtWidgets import QDialogButtonBox
from PySide6.QtWidgets import QTableWidget
from PySide6.QtWidgets import QTableWidgetItem


class CheckboxTrackListingDialog(QDialog, Ui_CheckboxTrackListingDialog):
def __init__(self, parent, dupe_groups: List[List[Dict]]):
super().__init__(parent)
self.setupUi(self)

self.dupe_groups = dupe_groups
self.okCancelbuttonBox.button(QDialogButtonBox.Ok).setText("Finish")
self.tableWidget = CheckboxTableWidget(self)
self.leftButton.setEnabled(False)
self.rightButton.setEnabled(False)
self.tableWidget.table_idx_changed.connect(self.on_table_idx_changed)
self.leftButton.clicked.connect(self.left_button_clicked)
self.rightButton.clicked.connect(self.right_button_clicked)
self.tableWidget.table_idx = 0

self.items_to_remove = []

@Slot()
def left_button_clicked(self):
self.tableWidget.table_idx -= 1

@Slot()
def right_button_clicked(self):
self.tableWidget.table_idx += 1

def on_table_idx_changed(self, new_idx):
self.tableWidget.setRowCount(0)
self.tableWidget.repopulate_table()
self.leftButton.setEnabled(new_idx > 0)
self.rightButton.setEnabled(new_idx < (len(self.dupe_groups) - 1))
self.pageNumberLabel.setText(f"Page {new_idx + 1} of {len(self.dupe_groups)}")

def accept(self):
super().accept()

def reject(self):
super().reject()


class CheckboxTableWidget(QTableWidget):
table_idx_changed = Signal(int)

def __init__(self, parent=None):
super().__init__(parent)

self.setGeometry(QRect(30, 50, 860, 440))
self._table_idx = -1
self.cellChanged.connect(self.on_cell_changed)
self.setColumnCount(6)
self.setHorizontalHeaderLabels(["Remove?", "Artist", "Title", "Album", "Duration", "Artwork"])

@Property(int, notify=table_idx_changed)
def table_idx(self):
return self._table_idx

@table_idx.setter
def table_idx(self, value):
if self._table_idx != value:
self._table_idx = value
self.table_idx_changed.emit(self._table_idx)

def repopulate_table(self):
for row_idx, track in enumerate(self.parentWidget().dupe_groups[self.table_idx]):
data_list: List[str] = [
track["artist"],
track["title"],
track["album"],
track["duration"],
track["thumbnail"],
]
row_items: List[QTableWidgetItem] = [QTableWidgetItem(data) for data in data_list]

# Insert the blank row
self.insertRow(row_idx)

checkbox_item = QTableWidgetItem()
checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
checkbox_item.setCheckState(
Qt.Checked
if self.parentWidget().dupe_groups[self.table_idx][row_idx].get("checked", False)
else Qt.Unchecked
)
checkbox_item.setBackground(QBrush(QColorConstants.Gray))
self.setItem(row_idx, 0, checkbox_item)

for col_idx, item in enumerate(row_items):
if col_idx == 4:
r = requests.get(data_list[4])
img = QImage()
img.loadFromData(r.content)
pixmap = QPixmap.fromImage(img)

item.setIcon(pixmap)
item.setSizeHint(QSize(64, 64))
item.setText("")
# Set items in the new row
self.setItem(row_idx, col_idx + 1, item)

self.resizeColumnsToContents()

@Slot(int, int)
def on_cell_changed(self, row, col):
if col != 0:
# Not a checkbox
return
if self.item(row, col).checkState() == Qt.Checked:
self.parentWidget().dupe_groups[self.table_idx][row]["checked"] = True
elif self.item(row, col).checkState() == Qt.Unchecked:
self.parentWidget().dupe_groups[self.table_idx][row]["checked"] = False
88 changes: 88 additions & 0 deletions gui/src/main/python/generated/ui_checkbox_track_listing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'checkbox_track_listing.ui'
##
## Created by: Qt User Interface Compiler version 6.6.3
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QHBoxLayout, QHeaderView, QLabel, QPushButton,
QSizePolicy, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)

class Ui_CheckboxTrackListingDialog(object):
def setupUi(self, CheckboxTrackListingDialog):
if not CheckboxTrackListingDialog.objectName():
CheckboxTrackListingDialog.setObjectName(u"CheckboxTrackListingDialog")
CheckboxTrackListingDialog.resize(924, 622)
self.verticalLayoutWidget = QWidget(CheckboxTrackListingDialog)
self.verticalLayoutWidget.setObjectName(u"verticalLayoutWidget")
self.verticalLayoutWidget.setGeometry(QRect(160, 520, 631, 81))
self.verticalLayout = QVBoxLayout(self.verticalLayoutWidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.navigationButtonLayout = QHBoxLayout()
self.navigationButtonLayout.setObjectName(u"navigationButtonLayout")
self.navigationButtonLayout.setContentsMargins(-1, 0, -1, -1)
self.leftButton = QPushButton(self.verticalLayoutWidget)
self.leftButton.setObjectName(u"leftButton")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.leftButton.sizePolicy().hasHeightForWidth())
self.leftButton.setSizePolicy(sizePolicy)

self.navigationButtonLayout.addWidget(self.leftButton)

self.rightButton = QPushButton(self.verticalLayoutWidget)
self.rightButton.setObjectName(u"rightButton")
sizePolicy.setHeightForWidth(self.rightButton.sizePolicy().hasHeightForWidth())
self.rightButton.setSizePolicy(sizePolicy)

self.navigationButtonLayout.addWidget(self.rightButton)


self.verticalLayout.addLayout(self.navigationButtonLayout)

self.okCancelbuttonBox = QDialogButtonBox(self.verticalLayoutWidget)
self.okCancelbuttonBox.setObjectName(u"okCancelbuttonBox")
self.okCancelbuttonBox.setOrientation(Qt.Horizontal)
self.okCancelbuttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)

self.verticalLayout.addWidget(self.okCancelbuttonBox)

self.tableWidget = QTableWidget(CheckboxTrackListingDialog)
self.tableWidget.setObjectName(u"tableWidget")
self.tableWidget.setGeometry(QRect(30, 40, 861, 441))
self.instructionsLabel = QLabel(CheckboxTrackListingDialog)
self.instructionsLabel.setObjectName(u"instructionsLabel")
self.instructionsLabel.setGeometry(QRect(60, 20, 821, 16))
self.pageNumberLabel = QLabel(CheckboxTrackListingDialog)
self.pageNumberLabel.setObjectName(u"pageNumberLabel")
self.pageNumberLabel.setGeometry(QRect(420, 490, 131, 16))

self.retranslateUi(CheckboxTrackListingDialog)
self.okCancelbuttonBox.accepted.connect(CheckboxTrackListingDialog.accept)
self.okCancelbuttonBox.rejected.connect(CheckboxTrackListingDialog.reject)

QMetaObject.connectSlotsByName(CheckboxTrackListingDialog)
# setupUi

def retranslateUi(self, CheckboxTrackListingDialog):
CheckboxTrackListingDialog.setWindowTitle(QCoreApplication.translate("CheckboxTrackListingDialog", u"Select Tracks to Remove", None))
self.leftButton.setText(QCoreApplication.translate("CheckboxTrackListingDialog", u"\u2190", None))
self.rightButton.setText(QCoreApplication.translate("CheckboxTrackListingDialog", u"\u2192", None))
self.instructionsLabel.setText(QCoreApplication.translate("CheckboxTrackListingDialog", u"The following tracks are SIMLIAR to each other by artist and title. Select the track(s) you want to remove:", None))
self.pageNumberLabel.setText(QCoreApplication.translate("CheckboxTrackListingDialog", u"Page", None))
# retranslateUi

2 changes: 1 addition & 1 deletion gui/src/main/python/generated/ui_main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def setupUi(self, MainWindow):
"}")
self.playlistFunctionsLabel = QLabel(self.centralWidget)
self.playlistFunctionsLabel.setObjectName(u"playlistFunctionsLabel")
self.playlistFunctionsLabel.setGeometry(QRect(40, 210, 101, 20))
self.playlistFunctionsLabel.setGeometry(QRect(40, 210, 131, 20))
self.accountWidget = QWidget(self.centralWidget)
self.accountWidget.setObjectName(u"accountWidget")
self.accountWidget.setGeometry(QRect(680, 80, 171, 121))
Expand Down
6 changes: 3 additions & 3 deletions gui/src/main/python/generated/ui_playlist_selection_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ def setupUi(self, PlaylistSelectionDialog):
PlaylistSelectionDialog.resize(400, 300)
self.buttonBox = QDialogButtonBox(PlaylistSelectionDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setGeometry(QRect(30, 240, 341, 32))
self.buttonBox.setGeometry(QRect(30, 260, 341, 32))
self.buttonBox.setOrientation(Qt.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
self.playlistList = QListWidget(PlaylistSelectionDialog)
self.playlistList.setObjectName(u"playlistList")
self.playlistList.setGeometry(QRect(70, 30, 256, 192))
self.playlistList.setGeometry(QRect(25, 21, 351, 221))
self.playlistList.setSelectionRectVisible(False)
self.playlistList.setSortingEnabled(True)
self.shuffleCheckBox = QCheckBox(PlaylistSelectionDialog)
self.shuffleCheckBox.setObjectName(u"shuffleCheckBox")
self.shuffleCheckBox.setGeometry(QRect(90, 240, 70, 17))
self.shuffleCheckBox.setGeometry(QRect(90, 260, 70, 17))

self.retranslateUi(PlaylistSelectionDialog)
self.buttonBox.accepted.connect(PlaylistSelectionDialog.accept)
Expand Down
74 changes: 74 additions & 0 deletions gui/src/main/python/generated/ui_track_listing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'track_listing.ui'
##
## Created by: Qt User Interface Compiler version 6.6.3
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QHeaderView, QLabel, QSizePolicy,
QTableWidget, QTableWidgetItem, QWidget)

class Ui_TrackListingDialog(object):
def setupUi(self, TrackListingDialog):
if not TrackListingDialog.objectName():
TrackListingDialog.setObjectName(u"TrackListingDialog")
TrackListingDialog.resize(924, 622)
self.buttonBox = QDialogButtonBox(TrackListingDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setGeometry(QRect(370, 560, 161, 32))
self.buttonBox.setOrientation(Qt.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
self.trackListTable = QTableWidget(TrackListingDialog)
if (self.trackListTable.columnCount() < 5):
self.trackListTable.setColumnCount(5)
__qtablewidgetitem = QTableWidgetItem()
self.trackListTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem()
self.trackListTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem()
self.trackListTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.trackListTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
__qtablewidgetitem4 = QTableWidgetItem()
self.trackListTable.setHorizontalHeaderItem(4, __qtablewidgetitem4)
self.trackListTable.setObjectName(u"trackListTable")
self.trackListTable.setGeometry(QRect(30, 70, 861, 441))
self.trackListTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.trackListTable.setColumnCount(5)
self.descriptionLabel = QLabel(TrackListingDialog)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.descriptionLabel.setGeometry(QRect(30, 0, 851, 61))

self.retranslateUi(TrackListingDialog)
self.buttonBox.accepted.connect(TrackListingDialog.accept)
self.buttonBox.rejected.connect(TrackListingDialog.reject)

QMetaObject.connectSlotsByName(TrackListingDialog)
# setupUi

def retranslateUi(self, TrackListingDialog):
TrackListingDialog.setWindowTitle(QCoreApplication.translate("TrackListingDialog", u"Remove Exact Duplicates", None))
___qtablewidgetitem = self.trackListTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TrackListingDialog", u"Artist", None));
___qtablewidgetitem1 = self.trackListTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("TrackListingDialog", u"Title", None));
___qtablewidgetitem2 = self.trackListTable.horizontalHeaderItem(2)
___qtablewidgetitem2.setText(QCoreApplication.translate("TrackListingDialog", u"Album", None));
___qtablewidgetitem3 = self.trackListTable.horizontalHeaderItem(3)
___qtablewidgetitem3.setText(QCoreApplication.translate("TrackListingDialog", u"Duration", None));
___qtablewidgetitem4 = self.trackListTable.horizontalHeaderItem(4)
___qtablewidgetitem4.setText(QCoreApplication.translate("TrackListingDialog", u"Artwork", None));
self.descriptionLabel.setText(QCoreApplication.translate("TrackListingDialog", u"The following tracks will be removed because they are exact duplicates of another track in the playlist:", None))
# retranslateUi

3 changes: 1 addition & 2 deletions gui/src/main/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from PySide6.QtWidgets import QMessageBox
from remove_duplicates_dialog import RemoveDuplicatesDialog
from sort_playlists_dialog import SortPlaylistsDialog
from ytmusic_deleter import constants as const
from ytmusic_deleter import common as const
from ytmusicapi import YTMusic
from ytmusicapi.auth.oauth import OAuthCredentials
from ytmusicapi.auth.oauth import RefreshingToken
Expand Down Expand Up @@ -343,7 +343,6 @@ def launch_process(self, args: List[str]):
+ (["-v"] if self.verbose_logging else [])
+ args
)
print(cli_args)
self.message(f"Executing process: {CLI_EXECUTABLE} {' '.join(cli_args)}")
self.p.start(CLI_EXECUTABLE, cli_args)
self.progress_dialog = ProgressDialog(self)
Expand Down
Loading

0 comments on commit 98085cb

Please sign in to comment.