Youtube-Comment-Export 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- youtube_comment_export-0.1.0.dist-info/METADATA +103 -0
- youtube_comment_export-0.1.0.dist-info/RECORD +19 -0
- youtube_comment_export-0.1.0.dist-info/WHEEL +5 -0
- youtube_comment_export-0.1.0.dist-info/entry_points.txt +2 -0
- youtube_comment_export-0.1.0.dist-info/licenses/License +21 -0
- youtube_comment_export-0.1.0.dist-info/top_level.txt +1 -0
- ytComments/__init__.py +5 -0
- ytComments/gui/__init__.py +19 -0
- ytComments/gui/about.py +131 -0
- ytComments/gui/app.py +97 -0
- ytComments/gui/error.py +83 -0
- ytComments/gui/main.py +170 -0
- ytComments/gui/menu.py +26 -0
- ytComments/gui/progressbar.py +101 -0
- ytComments/gui/settings.py +186 -0
- ytComments/gui/update.py +158 -0
- ytComments/metadata.py +22 -0
- ytComments/settingsManager.py +172 -0
- ytComments/youtube.py +546 -0
ytComments/gui/menu.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QMenuBar, QMainWindow
|
|
2
|
+
from .about import AboutWindow
|
|
3
|
+
from .settings import SettingsWindow
|
|
4
|
+
|
|
5
|
+
class MenuBar(QMenuBar):
|
|
6
|
+
"""Menu bar of the application."""
|
|
7
|
+
def __init__(self, parent: QMainWindow):
|
|
8
|
+
super().__init__(parent)
|
|
9
|
+
self.app = parent
|
|
10
|
+
|
|
11
|
+
# Menu Settings
|
|
12
|
+
self.settings_dialog = SettingsWindow(self)
|
|
13
|
+
settings_menu = self.addAction("&Settings")
|
|
14
|
+
settings_menu.triggered.connect(self.show_settings)
|
|
15
|
+
|
|
16
|
+
# Menu About
|
|
17
|
+
self.about_dialog = AboutWindow(self)
|
|
18
|
+
help_menu = self.addAction("&About")
|
|
19
|
+
help_menu.triggered.connect(self.about_dialog.exec)
|
|
20
|
+
|
|
21
|
+
def show_settings(self):
|
|
22
|
+
"""Show the Settings window."""
|
|
23
|
+
x = self.app.geometry().x() - 100
|
|
24
|
+
y = self.app.geometry().y() - 100
|
|
25
|
+
self.settings_dialog.setGeometry(x, y, 270, 200)
|
|
26
|
+
self.settings_dialog.exec()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from PySide6.QtWidgets import (
|
|
3
|
+
QApplication, QDialog, QProgressBar, QLabel, QVBoxLayout, QPushButton
|
|
4
|
+
)
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
|
|
7
|
+
class LoadingWindow(QDialog):
|
|
8
|
+
"""Window to show the progress of a long-running operation."""
|
|
9
|
+
def __init__(self, parent=None, countdown:bool = False):
|
|
10
|
+
if QApplication.instance() is None:
|
|
11
|
+
self.qapp = QApplication(sys.argv)
|
|
12
|
+
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.setWindowTitle("Loading...")
|
|
15
|
+
self.countdown = countdown
|
|
16
|
+
|
|
17
|
+
if parent:
|
|
18
|
+
self.parent = parent
|
|
19
|
+
x = self.parent.app.geometry().x()
|
|
20
|
+
y = self.parent.app.geometry().y()
|
|
21
|
+
else:
|
|
22
|
+
x = 100
|
|
23
|
+
y = 100
|
|
24
|
+
self.setGeometry(x, y, 300, 100)
|
|
25
|
+
|
|
26
|
+
# Delete the Windows title buttons
|
|
27
|
+
self.setWindowFlags(Qt.Window | Qt.WindowTitleHint | Qt.WindowStaysOnTopHint)
|
|
28
|
+
|
|
29
|
+
# Initialize a close event
|
|
30
|
+
self.closing = False
|
|
31
|
+
|
|
32
|
+
# Create the progress bar
|
|
33
|
+
self.progress_bar = QProgressBar(self)
|
|
34
|
+
self.progress_bar.setMinimum(0)
|
|
35
|
+
self.progress_bar.setMaximum(100)
|
|
36
|
+
self.progress_bar.setStyleSheet("""
|
|
37
|
+
QProgressBar {
|
|
38
|
+
background-color: #f0f0f0; /* Background color */
|
|
39
|
+
border: 2px solid #8f8f8f; /* Border color */
|
|
40
|
+
border-radius: 5px; /* Rounded corners */
|
|
41
|
+
height: 15px; /* Height of the progress bar */
|
|
42
|
+
text-align: center; /* Center the text */
|
|
43
|
+
color: #000000; /* Text color */
|
|
44
|
+
}
|
|
45
|
+
QProgressBar::chunk {
|
|
46
|
+
background-color: #3c8dbc; /* Background color of the filled chunk */
|
|
47
|
+
border-radius: 5px; /* Rounded corners of the filled chunk */
|
|
48
|
+
}
|
|
49
|
+
""")
|
|
50
|
+
|
|
51
|
+
# Add an explanatory text
|
|
52
|
+
self.label = QLabel("Downloading...", self)
|
|
53
|
+
self.label.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
|
|
54
|
+
|
|
55
|
+
# Add a countdown label
|
|
56
|
+
if self.countdown:
|
|
57
|
+
self.countdown_label = QLabel()
|
|
58
|
+
self.countdown_label.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
|
|
59
|
+
|
|
60
|
+
# Add a cancel button
|
|
61
|
+
msg = "Stop the download earlier, only the downloaded comments will be added to the old save"
|
|
62
|
+
self.cancel_button = QPushButton("Cancel")
|
|
63
|
+
self.cancel_button.clicked.connect(self.terminate)
|
|
64
|
+
self.cancel_button.setToolTip(msg)
|
|
65
|
+
|
|
66
|
+
# Create the layout
|
|
67
|
+
layout = QVBoxLayout()
|
|
68
|
+
layout.addWidget(self.progress_bar)
|
|
69
|
+
layout.addWidget(self.label)
|
|
70
|
+
if self.countdown: layout.addWidget(self.countdown_label)
|
|
71
|
+
layout.addWidget(self.cancel_button)
|
|
72
|
+
layout.setAlignment(Qt.AlignCenter)
|
|
73
|
+
self.setLayout(layout)
|
|
74
|
+
|
|
75
|
+
def update_progress(self, value, duration:int | None = None):
|
|
76
|
+
"""Update the progress bar value."""
|
|
77
|
+
self.progress_bar.setValue(value)
|
|
78
|
+
|
|
79
|
+
# Update the countdown label
|
|
80
|
+
if self.countdown and duration is not None:
|
|
81
|
+
hours = int(duration // 3600)
|
|
82
|
+
minutes = int((duration % 3600) // 60)
|
|
83
|
+
seconds = int(duration % 60)
|
|
84
|
+
|
|
85
|
+
self.countdown_label.setText(
|
|
86
|
+
f"Remaining estimated time : {hours:02}h:{minutes:02}m:{seconds:02}s"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
QApplication.processEvents()
|
|
90
|
+
|
|
91
|
+
def setText(self, text):
|
|
92
|
+
"""Update the window title."""
|
|
93
|
+
self.label.setText(text)
|
|
94
|
+
|
|
95
|
+
def terminate(self):
|
|
96
|
+
"""Close the window."""
|
|
97
|
+
self.closing = True
|
|
98
|
+
self.close()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from PySide6 import QtWidgets, QtGui, QtCore
|
|
2
|
+
from .error import run_error
|
|
3
|
+
|
|
4
|
+
class SettingsWindow(QtWidgets.QDialog):
|
|
5
|
+
"""Window for settings."""
|
|
6
|
+
def __init__(self, parent=None):
|
|
7
|
+
super().__init__(parent)
|
|
8
|
+
self.menu = parent
|
|
9
|
+
self.setWindowTitle("Settings")
|
|
10
|
+
self.setModal(True)
|
|
11
|
+
|
|
12
|
+
# Settings Layout
|
|
13
|
+
layout = QtWidgets.QVBoxLayout(self)
|
|
14
|
+
layout.addLayout(self.init_color())
|
|
15
|
+
layout.addLayout(self.init_highlight())
|
|
16
|
+
layout.addLayout(self.init_max_sheets())
|
|
17
|
+
layout.addLayout(self.init_date_format())
|
|
18
|
+
layout.addLayout(self.init_auto_update())
|
|
19
|
+
layout.addSpacing(10)
|
|
20
|
+
layout.addLayout(self.init_buttons())
|
|
21
|
+
|
|
22
|
+
def init_color(self):
|
|
23
|
+
"""Initialize Hex color code for the background of the comments on Excel."""
|
|
24
|
+
msg = "Hex color code for the background of the comments on Excel"
|
|
25
|
+
regex = QtCore.QRegularExpression(r'^#[0-9A-Fa-f]{6}$')
|
|
26
|
+
validator = QtGui.QRegularExpressionValidator(regex)
|
|
27
|
+
|
|
28
|
+
# Color Label
|
|
29
|
+
color_label = QtWidgets.QLabel("Background color :")
|
|
30
|
+
color_label.setToolTip(msg)
|
|
31
|
+
|
|
32
|
+
# Color1 input
|
|
33
|
+
self.color1_input = QtWidgets.QLineEdit()
|
|
34
|
+
self.color1_input.setText(self.menu.app.yt.settings.bg_color[0])
|
|
35
|
+
self.color1_input.setPlaceholderText("#C5D9F1")
|
|
36
|
+
self.color1_input.setToolTip(msg)
|
|
37
|
+
self.color1_input.setValidator(validator)
|
|
38
|
+
|
|
39
|
+
# Color2 input
|
|
40
|
+
self.color2_input = QtWidgets.QLineEdit()
|
|
41
|
+
self.color2_input.setText(self.menu.app.yt.settings.bg_color[1])
|
|
42
|
+
self.color2_input.setPlaceholderText("#FDE9D9")
|
|
43
|
+
self.color2_input.setToolTip(msg)
|
|
44
|
+
self.color2_input.setValidator(validator)
|
|
45
|
+
|
|
46
|
+
# Color Layout
|
|
47
|
+
color_layout = QtWidgets.QHBoxLayout()
|
|
48
|
+
color_layout.addWidget(color_label)
|
|
49
|
+
color_layout.addWidget(self.color1_input)
|
|
50
|
+
color_layout.addWidget(self.color2_input)
|
|
51
|
+
return color_layout
|
|
52
|
+
|
|
53
|
+
def init_highlight(self):
|
|
54
|
+
"""Initialize the toggle for the background color."""
|
|
55
|
+
msg = "Check to activate the background color"
|
|
56
|
+
|
|
57
|
+
# Highlight label
|
|
58
|
+
highlight_label = QtWidgets.QLabel("Background color :")
|
|
59
|
+
highlight_label.setToolTip(msg)
|
|
60
|
+
|
|
61
|
+
# Highlight toggle
|
|
62
|
+
self.highlight_toggle = QtWidgets.QCheckBox()
|
|
63
|
+
self.highlight_toggle.setChecked(self.menu.app.yt.settings.bg_highlight)
|
|
64
|
+
self.highlight_toggle.setToolTip(msg)
|
|
65
|
+
|
|
66
|
+
# Highlight layout
|
|
67
|
+
highlight_layout = QtWidgets.QHBoxLayout()
|
|
68
|
+
highlight_layout.addWidget(highlight_label)
|
|
69
|
+
highlight_layout.addWidget(self.highlight_toggle)
|
|
70
|
+
return highlight_layout
|
|
71
|
+
|
|
72
|
+
def init_max_sheets(self):
|
|
73
|
+
"""Initialize the maximum number of sheets per file to export."""
|
|
74
|
+
msg = "Maximum number of sheets per file to export"
|
|
75
|
+
|
|
76
|
+
# Sheets label
|
|
77
|
+
sheets_label = QtWidgets.QLabel("Sheets per file :")
|
|
78
|
+
sheets_label.setToolTip(msg)
|
|
79
|
+
|
|
80
|
+
# Sheets input
|
|
81
|
+
self.sheets_input = QtWidgets.QLineEdit()
|
|
82
|
+
self.sheets_input.setText(str(self.menu.app.yt.settings.max_sheets))
|
|
83
|
+
self.sheets_input.setToolTip(msg)
|
|
84
|
+
|
|
85
|
+
# Sheets layout
|
|
86
|
+
sheets_layout = QtWidgets.QHBoxLayout()
|
|
87
|
+
sheets_layout.addWidget(sheets_label)
|
|
88
|
+
sheets_layout.addWidget(self.sheets_input)
|
|
89
|
+
return sheets_layout
|
|
90
|
+
|
|
91
|
+
def init_date_format(self):
|
|
92
|
+
"""Initialize the date format."""
|
|
93
|
+
msg = "Format of the date in the comments"
|
|
94
|
+
|
|
95
|
+
# Date label
|
|
96
|
+
date_label = QtWidgets.QLabel("Date format :")
|
|
97
|
+
date_label.setToolTip(msg)
|
|
98
|
+
|
|
99
|
+
# Date input
|
|
100
|
+
self.date_input = QtWidgets.QLineEdit()
|
|
101
|
+
self.date_input.setText(self.menu.app.yt.settings.date_format)
|
|
102
|
+
self.date_input.setToolTip(msg)
|
|
103
|
+
|
|
104
|
+
# Date layout
|
|
105
|
+
date_layout = QtWidgets.QHBoxLayout()
|
|
106
|
+
date_layout.addWidget(date_label)
|
|
107
|
+
date_layout.addWidget(self.date_input)
|
|
108
|
+
return date_layout
|
|
109
|
+
|
|
110
|
+
def init_auto_update(self):
|
|
111
|
+
"""Initialize the toggle for the automatic update."""
|
|
112
|
+
msg = "Allow or not the program to search for software updates at startup"
|
|
113
|
+
|
|
114
|
+
# Update label
|
|
115
|
+
update_label = QtWidgets.QLabel("Search update at startup :")
|
|
116
|
+
update_label.setToolTip(msg)
|
|
117
|
+
|
|
118
|
+
# Update toggle
|
|
119
|
+
self.update_toggle = QtWidgets.QCheckBox()
|
|
120
|
+
self.update_toggle.setChecked(self.menu.app.yt.settings.auto_update)
|
|
121
|
+
self.update_toggle.setToolTip(msg)
|
|
122
|
+
|
|
123
|
+
# Update layout
|
|
124
|
+
update_layout = QtWidgets.QHBoxLayout()
|
|
125
|
+
update_layout.addWidget(update_label)
|
|
126
|
+
update_layout.addWidget(self.update_toggle)
|
|
127
|
+
return update_layout
|
|
128
|
+
|
|
129
|
+
def init_buttons(self):
|
|
130
|
+
"""Initialize the OK and Cancel buttons."""
|
|
131
|
+
# OK button
|
|
132
|
+
ok_button = QtWidgets.QPushButton("OK")
|
|
133
|
+
ok_button.clicked.connect(self.on_ok)
|
|
134
|
+
|
|
135
|
+
# Cancel button
|
|
136
|
+
cancel_button = QtWidgets.QPushButton("Cancel")
|
|
137
|
+
cancel_button.clicked.connect(self.on_cancel)
|
|
138
|
+
|
|
139
|
+
# Buttons layout
|
|
140
|
+
buttons_layout = QtWidgets.QHBoxLayout()
|
|
141
|
+
buttons_layout.addWidget(ok_button)
|
|
142
|
+
buttons_layout.addWidget(cancel_button)
|
|
143
|
+
return buttons_layout
|
|
144
|
+
|
|
145
|
+
def on_ok(self):
|
|
146
|
+
"""Manage the click on the OK button."""
|
|
147
|
+
|
|
148
|
+
# Validate the color inputs
|
|
149
|
+
try:
|
|
150
|
+
color1 = str(self.color1_input.text()).upper()
|
|
151
|
+
color2 = str(self.color2_input.text()).upper()
|
|
152
|
+
if color1 == "":
|
|
153
|
+
color1 = "#C5D9F1"
|
|
154
|
+
self.color1_input.setText("#C5D9F1")
|
|
155
|
+
if color2 == "":
|
|
156
|
+
color2 = "#FDE9D9"
|
|
157
|
+
self.color2_input.setText("#FDE9D9")
|
|
158
|
+
|
|
159
|
+
self.menu.app.yt.settings.bg_color = (color1, color2)
|
|
160
|
+
except ValueError as e:
|
|
161
|
+
run_error(e)
|
|
162
|
+
self.color1_input.setText(str(self.menu.app.yt.settings.bg_color[0]))
|
|
163
|
+
self.color2_input.setText(str(self.menu.app.yt.settings.bg_color[1]))
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Validate the max sheets inputs
|
|
167
|
+
try:
|
|
168
|
+
max_sheets = int(self.sheets_input.text())
|
|
169
|
+
self.menu.app.yt.settings.max_sheets = max_sheets
|
|
170
|
+
except ValueError as e:
|
|
171
|
+
run_error("The maximum number of sheets must be at least 2")
|
|
172
|
+
self.sheets_input.setText(str(self.menu.app.yt.settings.max_sheets))
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Validate the other settings
|
|
176
|
+
self.menu.app.yt.settings.bg_highlight = bool(self.highlight_toggle.isChecked())
|
|
177
|
+
self.menu.app.yt.settings.date_format = str(self.date_input.text())
|
|
178
|
+
self.menu.app.yt.settings.auto_update = bool(self.update_toggle.isChecked())
|
|
179
|
+
self.menu.app.yt.settings.save()
|
|
180
|
+
self.accept()
|
|
181
|
+
|
|
182
|
+
def on_cancel(self):
|
|
183
|
+
"""Manage the click on the Cancel button."""
|
|
184
|
+
self.reject()
|
|
185
|
+
|
|
186
|
+
|
ytComments/gui/update.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from PySide6.QtWidgets import QMessageBox
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
from .progressbar import LoadingWindow
|
|
7
|
+
import ytComments.metadata as metadata
|
|
8
|
+
|
|
9
|
+
class UpdateManager:
|
|
10
|
+
"""Update Downloader of the software"""
|
|
11
|
+
def __init__(self, repo_owner, repo_name, parent=None):
|
|
12
|
+
self.about = parent
|
|
13
|
+
self.repo_owner = repo_owner
|
|
14
|
+
self.repo_name = repo_name
|
|
15
|
+
self._file_size = 0
|
|
16
|
+
self._downloaded_size = 0
|
|
17
|
+
self.dir_update = "update"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def file_size(self):
|
|
21
|
+
"""Size of the file to download."""
|
|
22
|
+
return self._file_size
|
|
23
|
+
|
|
24
|
+
@file_size.setter
|
|
25
|
+
def file_size(self, value:int):
|
|
26
|
+
if not isinstance(value, int):
|
|
27
|
+
msg = "The file size must be an integer"
|
|
28
|
+
raise TypeError(
|
|
29
|
+
msg
|
|
30
|
+
)
|
|
31
|
+
self._file_size = value
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def downloaded_size(self):
|
|
35
|
+
"""Size of the downloaded file."""
|
|
36
|
+
return self._downloaded_size
|
|
37
|
+
|
|
38
|
+
@downloaded_size.setter
|
|
39
|
+
def downloaded_size(self, value:int):
|
|
40
|
+
if not isinstance(value, int):
|
|
41
|
+
msg = "The file size must be an integer"
|
|
42
|
+
raise TypeError(
|
|
43
|
+
msg
|
|
44
|
+
)
|
|
45
|
+
self._downloaded_size = value
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def progress(self):
|
|
49
|
+
"""Percentage of download."""
|
|
50
|
+
if self.file_size == 0:
|
|
51
|
+
return 0
|
|
52
|
+
return int(self.downloaded_size / self.file_size * 100)
|
|
53
|
+
|
|
54
|
+
def get_latest_release_info(self, extension=".exe"):
|
|
55
|
+
"""
|
|
56
|
+
Retrieves the latest release information from GitHub,
|
|
57
|
+
including the tag and the URL of the .exe file.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
tuple: A tuple containing the latest version tag and
|
|
61
|
+
the URL of the .exe file, or (None, None) if an error occurs.
|
|
62
|
+
"""
|
|
63
|
+
url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/releases/latest"
|
|
64
|
+
try:
|
|
65
|
+
response = requests.get(url)
|
|
66
|
+
response.raise_for_status() # Throws an exception for HTTP error codes
|
|
67
|
+
data = response.json()
|
|
68
|
+
tag_name = data.get("tag_name")
|
|
69
|
+
assets = data.get("assets", [])
|
|
70
|
+
exe_url = None
|
|
71
|
+
for asset in assets:
|
|
72
|
+
if asset.get("name", "").lower().endswith(extension):
|
|
73
|
+
exe_url = asset.get("browser_download_url")
|
|
74
|
+
break
|
|
75
|
+
return tag_name, exe_url
|
|
76
|
+
except requests.exceptions.RequestException as e:
|
|
77
|
+
print(f"Error while retrieving the latest version from GitHub : {e}")
|
|
78
|
+
return None, None
|
|
79
|
+
|
|
80
|
+
def check_updates(self):
|
|
81
|
+
"""
|
|
82
|
+
Checks if an update is available.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
bool: True if an update is available, False otherwise.
|
|
86
|
+
"""
|
|
87
|
+
latest_tag, exe_url = self.get_latest_release_info()
|
|
88
|
+
|
|
89
|
+
if not latest_tag or not exe_url:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
latest_version = Version(latest_tag.lstrip('v'))
|
|
93
|
+
current_version = Version(metadata.version)
|
|
94
|
+
|
|
95
|
+
return latest_version > current_version
|
|
96
|
+
|
|
97
|
+
def update_software(self):
|
|
98
|
+
"""Updates the software by downloading the .exe file from GitHub."""
|
|
99
|
+
|
|
100
|
+
print("Updating software...")
|
|
101
|
+
_, exe_url = self.get_latest_release_info()
|
|
102
|
+
if not exe_url:
|
|
103
|
+
print("No .exe file founded in the latest version.")
|
|
104
|
+
return
|
|
105
|
+
print(f"Downloading the new version from : {exe_url}")
|
|
106
|
+
|
|
107
|
+
response = requests.get(exe_url, stream=True)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
self.file_size = int(response.headers.get("content-length", 0))
|
|
110
|
+
|
|
111
|
+
# Check if the program is an executable or a python file
|
|
112
|
+
if hasattr(sys, 'frozen'):
|
|
113
|
+
self.old_path = os.path.abspath(sys.executable) # .exe
|
|
114
|
+
else:
|
|
115
|
+
self.old_path = os.path.abspath(__file__) # .py
|
|
116
|
+
|
|
117
|
+
# Define the directory and file path for the update
|
|
118
|
+
new_filename = str(os.path.basename(exe_url))
|
|
119
|
+
self.new_filedir = os.path.join(
|
|
120
|
+
os.path.dirname(self.old_path),
|
|
121
|
+
self.dir_update
|
|
122
|
+
)
|
|
123
|
+
self.new_path = os.path.join(self.new_filedir, new_filename)
|
|
124
|
+
|
|
125
|
+
# Create the directory if it doesn't exist
|
|
126
|
+
os.makedirs(self.new_filedir, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
# Display the progress bar
|
|
129
|
+
loading_window = LoadingWindow(self.about)
|
|
130
|
+
loading_window.show()
|
|
131
|
+
|
|
132
|
+
# Download the updated file
|
|
133
|
+
with open(self.new_path, "wb") as f:
|
|
134
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
135
|
+
f.write(chunk)
|
|
136
|
+
self.downloaded_size += len(chunk)
|
|
137
|
+
loading_window.update_progress(self.progress)
|
|
138
|
+
print(f"Progress update : {self.progress}%", end="\r")
|
|
139
|
+
|
|
140
|
+
loading_window.close()
|
|
141
|
+
print("Download of the update completed.")
|
|
142
|
+
|
|
143
|
+
def show_file_location_message(self, file_path):
|
|
144
|
+
"""Shows a message to the user about the new version."""
|
|
145
|
+
msg_box = QMessageBox()
|
|
146
|
+
txt = f"""
|
|
147
|
+
The update has been downloaded here :\n\n
|
|
148
|
+
{file_path}\n
|
|
149
|
+
You can delete the old version.
|
|
150
|
+
"""
|
|
151
|
+
msg_box.setWindowTitle("Update completed")
|
|
152
|
+
msg_box.setText(txt)
|
|
153
|
+
msg_box.setIcon(QMessageBox.Information)
|
|
154
|
+
msg_box.setStandardButtons(QMessageBox.Ok)
|
|
155
|
+
|
|
156
|
+
# Display the message
|
|
157
|
+
msg_box.exec()
|
|
158
|
+
|
ytComments/metadata.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from configparser import ConfigParser
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def get_project_metadata():
|
|
5
|
+
"""Metadata grabber from setup.cfg"""
|
|
6
|
+
config = ConfigParser()
|
|
7
|
+
setup_cfg = Path(__file__).parent.parent / "setup.cfg"
|
|
8
|
+
|
|
9
|
+
with open(setup_cfg, "r", encoding="utf-8") as f:
|
|
10
|
+
config.read_file(f)
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
"version": config.get("metadata", "version"),
|
|
14
|
+
"author": config.get("metadata", "author"),
|
|
15
|
+
"name": config.get("metadata", "name"),
|
|
16
|
+
"description": config.get("metadata", "description"),
|
|
17
|
+
"url": config.get("metadata", "url"),
|
|
18
|
+
"license": config.get("metadata", "license"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Initialize the globals variables for an easy import
|
|
22
|
+
globals().update(get_project_metadata())
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
class Settings():
|
|
5
|
+
"""Settings Manager to save and apply settings to the software."""
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self._channel_url = 'https://www.youtube.com/@GabbPiano'
|
|
8
|
+
self._directory = Path.home() / 'Desktop'
|
|
9
|
+
self._bg_color = ('#C5D9F1', '#FDE9D9')
|
|
10
|
+
self._bg_highlight = True
|
|
11
|
+
self._max_sheets = 250
|
|
12
|
+
self._date_format = 'DD/MM/YYYY'
|
|
13
|
+
self._auto_update = True
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def channel_url(self):
|
|
17
|
+
"""URL of the YouTube channel."""
|
|
18
|
+
return self._channel_url
|
|
19
|
+
|
|
20
|
+
@channel_url.setter
|
|
21
|
+
def channel_url(self, value: str):
|
|
22
|
+
if value == None:
|
|
23
|
+
return
|
|
24
|
+
elif not isinstance(value, str):
|
|
25
|
+
msg = 'The channel URL must be a string'
|
|
26
|
+
raise TypeError(msg)
|
|
27
|
+
self._channel_url = str(value)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def directory(self):
|
|
31
|
+
"""Directory where the files will be saved."""
|
|
32
|
+
return self._directory
|
|
33
|
+
|
|
34
|
+
@directory.setter
|
|
35
|
+
def directory(self, value):
|
|
36
|
+
if value == None:
|
|
37
|
+
return
|
|
38
|
+
else:
|
|
39
|
+
path = Path(value)
|
|
40
|
+
|
|
41
|
+
if path.exists() and path.is_dir():
|
|
42
|
+
self._directory = path
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"The directory {value} does not exist"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def bg_color(self):
|
|
50
|
+
"""Hex color code for the background of the comments on Excel."""
|
|
51
|
+
return self._bg_color
|
|
52
|
+
|
|
53
|
+
@bg_color.setter
|
|
54
|
+
def bg_color(self, value):
|
|
55
|
+
if (len(value) != 2
|
|
56
|
+
or not isinstance(value, tuple)
|
|
57
|
+
or not isinstance(value[0], str)
|
|
58
|
+
or not isinstance(value[1], str)
|
|
59
|
+
):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"The value must be a tuple of two strings"
|
|
62
|
+
)
|
|
63
|
+
elif not (value[0].startswith('#') and len(value[0]) == 7):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"The first value must be a valid hex color code"
|
|
66
|
+
)
|
|
67
|
+
elif not (value[1].startswith('#') and len(value[1]) == 7):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"The second value must be a valid hex color code"
|
|
70
|
+
)
|
|
71
|
+
self._bg_color = value
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def bg_highlight(self):
|
|
75
|
+
"""Allow or not the background color to be highlighted."""
|
|
76
|
+
return self._bg_highlight
|
|
77
|
+
|
|
78
|
+
@bg_highlight.setter
|
|
79
|
+
def bg_highlight(self, value):
|
|
80
|
+
if not isinstance(value, bool):
|
|
81
|
+
return
|
|
82
|
+
self._bg_highlight = value
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def max_sheets(self):
|
|
86
|
+
"""Maximum number of sheets per file to export."""
|
|
87
|
+
return self._max_sheets
|
|
88
|
+
|
|
89
|
+
@max_sheets.setter
|
|
90
|
+
def max_sheets(self, value):
|
|
91
|
+
if not isinstance(value, int):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"The maximum number of sheets must be an integer"
|
|
94
|
+
)
|
|
95
|
+
elif value < 2:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"The maximum number of sheets must be at least 2"
|
|
98
|
+
)
|
|
99
|
+
self._max_sheets = value
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def date_format(self):
|
|
103
|
+
"""Format of the date in the comments."""
|
|
104
|
+
return self._date_format
|
|
105
|
+
|
|
106
|
+
@date_format.setter
|
|
107
|
+
def date_format(self, value):
|
|
108
|
+
if not isinstance(value, str):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"the date format must be a string. E.g: 'DD/MM/YY' ; 'YYYY-MM-DD'"
|
|
111
|
+
)
|
|
112
|
+
self._date_format = value
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def auto_update(self):
|
|
116
|
+
"""Allow or not the program to search for software updates at startup."""
|
|
117
|
+
return self._auto_update
|
|
118
|
+
|
|
119
|
+
@auto_update.setter
|
|
120
|
+
def auto_update(self, value):
|
|
121
|
+
if not isinstance(value, bool):
|
|
122
|
+
return
|
|
123
|
+
self._auto_update = value
|
|
124
|
+
|
|
125
|
+
def path_save(self):
|
|
126
|
+
"""Path to the saved parameters file."""
|
|
127
|
+
file = "yt_parameters.json"
|
|
128
|
+
folder = "Atem83"
|
|
129
|
+
path_file = Path.home() / "Documents" / folder / file
|
|
130
|
+
return path_file
|
|
131
|
+
|
|
132
|
+
def save(self):
|
|
133
|
+
"""Save the parameters in a configuration file."""
|
|
134
|
+
settings = {
|
|
135
|
+
'channel_url': self.channel_url,
|
|
136
|
+
'directory': str(self.directory),
|
|
137
|
+
'bg_color': self.bg_color,
|
|
138
|
+
'bg_highlight': self.bg_highlight,
|
|
139
|
+
'max_sheets': self.max_sheets,
|
|
140
|
+
'date_format': self.date_format,
|
|
141
|
+
'auto_update': self.auto_update
|
|
142
|
+
}
|
|
143
|
+
path_file = self.path_save()
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
# Create the folder if it doesn't exist
|
|
147
|
+
path_folder = Path(path_file).parent
|
|
148
|
+
path_folder.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
|
|
150
|
+
# Write my backup
|
|
151
|
+
with open(path_file, 'w', encoding='utf-8') as file:
|
|
152
|
+
json.dump(settings, file, indent=4, ensure_ascii=False)
|
|
153
|
+
except:
|
|
154
|
+
print("Save failed.")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
def load(self):
|
|
158
|
+
"""Load the parameters from the configuration file."""
|
|
159
|
+
path_file = self.path_save()
|
|
160
|
+
try:
|
|
161
|
+
with open(path_file, "r", encoding='utf-8') as file:
|
|
162
|
+
settings = json.load(file)
|
|
163
|
+
self.channel_url = settings["channel_url"]
|
|
164
|
+
self.directory = settings["directory"]
|
|
165
|
+
self.bg_color = tuple(settings["bg_color"])
|
|
166
|
+
self.bg_highlight = settings["bg_highlight"]
|
|
167
|
+
self.max_sheets = settings["max_sheets"]
|
|
168
|
+
self.date_format = settings["date_format"]
|
|
169
|
+
self.auto_update = settings["auto_update"]
|
|
170
|
+
except:
|
|
171
|
+
print("Load failed, the parameters by default will be used.")
|
|
172
|
+
return
|