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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Youtube-Comment-Export
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Export a YouTube channel Comments in a nice Excel format.
|
|
5
|
+
Home-page: https://github.com/Atem83/Youtube-Comment-Export
|
|
6
|
+
Author: Michaël AUGUSTE
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: License
|
|
13
|
+
Requires-Dist: yt_dlp
|
|
14
|
+
Requires-Dist: polars
|
|
15
|
+
Requires-Dist: xlsxwriter
|
|
16
|
+
Requires-Dist: colorama
|
|
17
|
+
Requires-Dist: PySide6
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
<h1 align="center">Youtube Comment Export</h1>
|
|
21
|
+
|
|
22
|
+
<h3 align="center">Export the comments of a YouTube channel in Excel files.</h3>
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/Youtube-Comment-Export)
|
|
27
|
+
<a href="https://opensource.org/license/mit"></a>
|
|
28
|
+
<a href="https://github.com/Atem83/Youtube-Comment-Export/archive/refs/heads/main.zip"></a>
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div align="center">
|
|
34
|
+
<div style="display: flex; justify-content: space-around;">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/Atem83/Youtube-Comment-Export/main/images/GUI light theme.png" alt="GUI Light Theme" style="width: 35%;">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/Atem83/Youtube-Comment-Export/main/images/GUI dark theme.png" alt="GUI Dark Theme" style="width: 35%;">
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<br>
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
<img src="https://raw.githubusercontent.com/Atem83/Youtube-Comment-Export/main/images/GUI Settings.png" alt="GUI Settings" style="width: 35%;">
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<h2 align="center"> Excel Main Sheet </h2>
|
|
47
|
+
|
|
48
|
+
<div align="center">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/Atem83/Youtube-Comment-Export/main/images/Excel videos sheet.png" alt="Excel videos sheet" style="width: 100%;">
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<h2 align="center"> Excel Comments Sheet </h2>
|
|
53
|
+
|
|
54
|
+
<div align="center">
|
|
55
|
+
<img src="https://raw.githubusercontent.com/Atem83/Youtube-Comment-Export/main/images/Excel comments sheet.png" alt="Excel comments sheet" style="width: 100%;">
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<h2 align="center"> Features </h2>
|
|
59
|
+
|
|
60
|
+
- Download data from a YouTube channel
|
|
61
|
+
- Download comments from a YouTube channel
|
|
62
|
+
- Export the data and comments in Excel files in a nice format
|
|
63
|
+
- Light ou Dark theme according to your OS theme
|
|
64
|
+
- Import old saves, this way :
|
|
65
|
+
- you can keep a more precise date of the comments
|
|
66
|
+
- you can keep comments even if they have been deleted
|
|
67
|
+
- sadly, it will not be possible to save downloading time because all the comments needs to be downloaded again
|
|
68
|
+
|
|
69
|
+
<h2 align="center"> Installation </h2>
|
|
70
|
+
|
|
71
|
+
<div align="center">
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
pip install Youtube-Comment-Export
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
[<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/Atem83/Youtube-Comment-Export?&color=green&label=Source%20Code&logo=Python&logoColor=yellow&style=for-the-badge" width="300">](https://github.com/Atem83/Youtube-Comment-Export/archive/refs/heads/main.zip)
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h2 align="center"> GUI Usage </h2>
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import ytComments
|
|
85
|
+
|
|
86
|
+
app = ytComments.App()
|
|
87
|
+
app.run()
|
|
88
|
+
```
|
|
89
|
+
<h2 align="center"> Python Usage </h2>
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import ytComments
|
|
93
|
+
|
|
94
|
+
channel_url = 'https://www.youtube.com/@GabbPiano'
|
|
95
|
+
path_save1 = r'C:\Users\User\Desktop\Save File 1.xlsx'
|
|
96
|
+
path_save2 = r'C:\Users\User\Desktop\Save File 2.xlsx'
|
|
97
|
+
path_save = [path_save1, path_save2]
|
|
98
|
+
|
|
99
|
+
yt = ytComments.yt_manager(channel_url, old_save=path_save)
|
|
100
|
+
yt.import_excel() # Import the old save data
|
|
101
|
+
yt.export_excel() # Export the data in Excel files
|
|
102
|
+
|
|
103
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
youtube_comment_export-0.1.0.dist-info/licenses/License,sha256=ZxJN3uGdLiUuaBcpc8ZRLNdJMOYjrvsIEzE-XdeHpcs,1094
|
|
2
|
+
ytComments/__init__.py,sha256=Dabtm_kYGtUcEjR9_ijRX9R6xbZfZzS85UFT25YUvXQ,143
|
|
3
|
+
ytComments/metadata.py,sha256=kXNbzQrcUDC_p1SylgM5AWmbLnUpPm6TJ-xpbuPm8SQ,744
|
|
4
|
+
ytComments/settingsManager.py,sha256=KWJ-S0NzDK9435rAPi50z1BhAM05EWcoR4uUgOgIZGc,5746
|
|
5
|
+
ytComments/youtube.py,sha256=ngDLkWwgqRQylH5NIrIwtYerwsqY4RZvEgKMbXFmHKs,23831
|
|
6
|
+
ytComments/gui/__init__.py,sha256=DlncYzZsj9SA99dXl_mnkn0MCW5DyM2qXboHH8sqAJ0,422
|
|
7
|
+
ytComments/gui/about.py,sha256=nF5g5f4rCiouVVucVhrNLFZBR7c8Oln41FTe3Ma4VNI,5100
|
|
8
|
+
ytComments/gui/app.py,sha256=U3zJYD3GM_AXWk5I2z34HKRXUc6rnCbsSVzzgDaYb7Q,3489
|
|
9
|
+
ytComments/gui/error.py,sha256=LnCj2mUOWcJlj46kPEPXlMF6PfwyPhiecDcfqqDRnd4,3134
|
|
10
|
+
ytComments/gui/main.py,sha256=WUolWkpl8b7i0ELdc7LlOD9WxqZE9GMnDXMIDCoKKqI,6381
|
|
11
|
+
ytComments/gui/menu.py,sha256=pgdFzRR4FZRmZ54ePl3gkiy3fitgjGnJTtvbqjpO1SE,938
|
|
12
|
+
ytComments/gui/progressbar.py,sha256=vZnR2XzBRfFESjj1KK-v5wQRQbBD7CvzuaAe_UoA6cI,3775
|
|
13
|
+
ytComments/gui/settings.py,sha256=MgegrkGU6vKshwuTjkeM4wNCEWkekkvLQe_6SLmbiys,7214
|
|
14
|
+
ytComments/gui/update.py,sha256=8RhfKdj-SRso4EwsHRcAZdDv5c9wCtOQxd4fZy95Cc4,5614
|
|
15
|
+
youtube_comment_export-0.1.0.dist-info/METADATA,sha256=tIpJpqaICF-zYIRxGoa-yQke4oE_jgtyiu2MEMPXF_U,3794
|
|
16
|
+
youtube_comment_export-0.1.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
17
|
+
youtube_comment_export-0.1.0.dist-info/entry_points.txt,sha256=gREMK5n_tans2C7FgvOYXX8DgzxYFWy3OM3ttEeAJw4,58
|
|
18
|
+
youtube_comment_export-0.1.0.dist-info/top_level.txt,sha256=f9W7wZM2h6hpL3Rv_Tyoi5vG6XCe-K54WGyFj48ycok,11
|
|
19
|
+
youtube_comment_export-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michaël AUGUSTE
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ytComments
|
ytComments/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .app import App
|
|
2
|
+
from .main import MainWindow
|
|
3
|
+
from .menu import MenuBar
|
|
4
|
+
from .progressbar import LoadingWindow
|
|
5
|
+
from .settings import SettingsWindow
|
|
6
|
+
from .about import AboutWindow
|
|
7
|
+
from .update import UpdateManager
|
|
8
|
+
from .error import run_error
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"App",
|
|
12
|
+
"MainWindow",
|
|
13
|
+
"MenuBar",
|
|
14
|
+
"LoadingWindow",
|
|
15
|
+
"SettingsWindow",
|
|
16
|
+
"AboutWindow",
|
|
17
|
+
"UpdateManager",
|
|
18
|
+
"run_error",
|
|
19
|
+
]
|
ytComments/gui/about.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import ytComments.metadata as metadata
|
|
3
|
+
from PySide6 import QtWidgets, QtCore, QtGui
|
|
4
|
+
from .update import UpdateManager
|
|
5
|
+
from .error import run_error
|
|
6
|
+
|
|
7
|
+
class AboutWindow(QtWidgets.QDialog):
|
|
8
|
+
"""About menu of the application"""
|
|
9
|
+
def __init__(self, parent):
|
|
10
|
+
super().__init__(parent)
|
|
11
|
+
self.menu = parent
|
|
12
|
+
self.setWindowTitle("About")
|
|
13
|
+
layout = QtWidgets.QVBoxLayout()
|
|
14
|
+
|
|
15
|
+
self.init_version()
|
|
16
|
+
layout.addLayout(self.layout_version)
|
|
17
|
+
|
|
18
|
+
self.init_author()
|
|
19
|
+
layout.addLayout(self.layout_author)
|
|
20
|
+
|
|
21
|
+
self.init_license()
|
|
22
|
+
layout.addLayout(self.layout_license)
|
|
23
|
+
|
|
24
|
+
self.init_description()
|
|
25
|
+
layout.addSpacing(10)
|
|
26
|
+
layout.addLayout(self.layout_description)
|
|
27
|
+
|
|
28
|
+
self.setLayout(layout)
|
|
29
|
+
|
|
30
|
+
def init_version(self):
|
|
31
|
+
"""Version number and search update button."""
|
|
32
|
+
self.layout_version = QtWidgets.QHBoxLayout()
|
|
33
|
+
version_label = QtWidgets.QLabel("Version : ")
|
|
34
|
+
version_text = QtWidgets.QLabel(metadata.version)
|
|
35
|
+
|
|
36
|
+
# Add the button to check for updates
|
|
37
|
+
self.check_update_button = QtWidgets.QPushButton("Check updates")
|
|
38
|
+
self.check_update_button.setStyleSheet(self.style_link())
|
|
39
|
+
self.check_update_button.clicked.connect(self.check_for_updates)
|
|
40
|
+
|
|
41
|
+
self.layout_version.addWidget(version_label)
|
|
42
|
+
self.layout_version.addWidget(version_text)
|
|
43
|
+
self.layout_version.addWidget(self.check_update_button)
|
|
44
|
+
self.layout_version.setAlignment(QtCore.Qt.AlignLeft)
|
|
45
|
+
|
|
46
|
+
def init_author(self):
|
|
47
|
+
"""Program Author."""
|
|
48
|
+
self.layout_author = QtWidgets.QHBoxLayout()
|
|
49
|
+
author_label = QtWidgets.QLabel("Author : ")
|
|
50
|
+
author_text = QtWidgets.QLabel(metadata.author)
|
|
51
|
+
|
|
52
|
+
self.layout_author.addWidget(author_label)
|
|
53
|
+
self.layout_author.addWidget(author_text)
|
|
54
|
+
self.layout_author.setAlignment(QtCore.Qt.AlignLeft)
|
|
55
|
+
|
|
56
|
+
def init_license(self):
|
|
57
|
+
"""License and code source url."""
|
|
58
|
+
self.layout_license = QtWidgets.QHBoxLayout()
|
|
59
|
+
license_label = QtWidgets.QLabel("License : ")
|
|
60
|
+
license_text = QtWidgets.QLabel(metadata.license)
|
|
61
|
+
|
|
62
|
+
url_button = QtWidgets.QPushButton("code source")
|
|
63
|
+
url_button.setStyleSheet(self.style_link())
|
|
64
|
+
url_button.clicked.connect(
|
|
65
|
+
lambda: QtGui.QDesktopServices.openUrl(QtCore.QUrl(metadata.url))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.layout_license.addWidget(license_label)
|
|
69
|
+
self.layout_license.addWidget(license_text)
|
|
70
|
+
self.layout_license.addWidget(url_button)
|
|
71
|
+
self.layout_license.setAlignment(QtCore.Qt.AlignLeft)
|
|
72
|
+
|
|
73
|
+
def init_description(self):
|
|
74
|
+
"""Software description."""
|
|
75
|
+
self.layout_description = QtWidgets.QHBoxLayout()
|
|
76
|
+
description_text = QtWidgets.QLabel(metadata.description)
|
|
77
|
+
self.layout_description.addWidget(description_text)
|
|
78
|
+
|
|
79
|
+
def style_link(self):
|
|
80
|
+
"""Style for links."""
|
|
81
|
+
css = """
|
|
82
|
+
QPushButton {
|
|
83
|
+
border: none; /* No border */
|
|
84
|
+
background: none; /* No background */
|
|
85
|
+
color: DodgerBlue; /* Text color */
|
|
86
|
+
text-decoration: underline; /* Underlined text to look like a link */
|
|
87
|
+
}
|
|
88
|
+
QPushButton:hover {
|
|
89
|
+
color: RoyalBlue; /* Text color on hover */
|
|
90
|
+
}
|
|
91
|
+
"""
|
|
92
|
+
return css
|
|
93
|
+
|
|
94
|
+
def extract_repo_info(self, url: str):
|
|
95
|
+
"""Extracts the username and repository name from a GitHub URL."""
|
|
96
|
+
pattern = r'https?://(?:www\.)?github\.com/([^/]+)/([^/]+)'
|
|
97
|
+
match = re.match(pattern, url)
|
|
98
|
+
|
|
99
|
+
if match:
|
|
100
|
+
repo_owner = match.group(1)
|
|
101
|
+
repo_name = match.group(2)
|
|
102
|
+
return repo_owner, repo_name
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"invalid URL"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def check_for_updates(self):
|
|
109
|
+
"""Check for update availability."""
|
|
110
|
+
self.check_update_button.setEnabled(False)
|
|
111
|
+
self.check_update_button.setText("Checking for updates...")
|
|
112
|
+
QtWidgets.QApplication.processEvents()
|
|
113
|
+
|
|
114
|
+
repo_owner, repo_name = self.extract_repo_info(metadata.url)
|
|
115
|
+
updater = UpdateManager(repo_owner, repo_name, self)
|
|
116
|
+
if updater.check_updates():
|
|
117
|
+
self.check_update_button.setText("Downloading update...")
|
|
118
|
+
QtWidgets.QApplication.processEvents()
|
|
119
|
+
try:
|
|
120
|
+
updater.update_software()
|
|
121
|
+
self.check_update_button.setText("Update completed")
|
|
122
|
+
updater.show_file_location_message(updater.new_filedir)
|
|
123
|
+
self.menu.app.close()
|
|
124
|
+
except Exception as e:
|
|
125
|
+
msg = "An error occurred during the update"
|
|
126
|
+
self.check_update_button.setText(msg)
|
|
127
|
+
run_error(msg, details = e)
|
|
128
|
+
else:
|
|
129
|
+
self.check_update_button.setText("Software up to date")
|
|
130
|
+
|
|
131
|
+
self.check_update_button.setEnabled(True)
|
ytComments/gui/app.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import ytComments.metadata as metadata
|
|
3
|
+
from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox
|
|
4
|
+
from ytComments import yt_manager
|
|
5
|
+
from .menu import MenuBar
|
|
6
|
+
from .main import MainWindow
|
|
7
|
+
from .update import UpdateManager
|
|
8
|
+
from .error import run_error
|
|
9
|
+
|
|
10
|
+
class App(QMainWindow):
|
|
11
|
+
"""Main application class."""
|
|
12
|
+
def __init__(self):
|
|
13
|
+
# Initialize QApplication
|
|
14
|
+
self.qapp = QApplication(sys.argv)
|
|
15
|
+
|
|
16
|
+
# Initialize QMainWindow
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.setWindowTitle(metadata.name)
|
|
19
|
+
#self.setGeometry(200, 200, 360, 400)
|
|
20
|
+
self.yt = yt_manager()
|
|
21
|
+
|
|
22
|
+
# Create the main window
|
|
23
|
+
self.main_frame = MainWindow(self)
|
|
24
|
+
self.setCentralWidget(self.main_frame)
|
|
25
|
+
|
|
26
|
+
# Create the menu bar
|
|
27
|
+
self.menu_bar = MenuBar(self)
|
|
28
|
+
self.setMenuBar(self.menu_bar)
|
|
29
|
+
|
|
30
|
+
# Connect the window resize event
|
|
31
|
+
self.resizeEvent = self.on_resize
|
|
32
|
+
|
|
33
|
+
# Allow the window to accept drag-and-drop events for files
|
|
34
|
+
self.setAcceptDrops(True)
|
|
35
|
+
|
|
36
|
+
def run(self):
|
|
37
|
+
"""Execution of the application."""
|
|
38
|
+
self.show()
|
|
39
|
+
self.check_for_updates()
|
|
40
|
+
sys.exit(self.qapp.exec())
|
|
41
|
+
|
|
42
|
+
def on_resize(self, event):
|
|
43
|
+
"""Execute actions when the main window is resized."""
|
|
44
|
+
#print(f"Dimensions of the window : {self.width()}x{self.height()}")
|
|
45
|
+
super().resizeEvent(event)
|
|
46
|
+
|
|
47
|
+
def dragEnterEvent(self, event):
|
|
48
|
+
"""Manage the drag-and-drop event.
|
|
49
|
+
Only accept files."""
|
|
50
|
+
if event.mimeData().hasUrls():
|
|
51
|
+
event.accept() # Accept the event
|
|
52
|
+
else:
|
|
53
|
+
event.ignore() # Ignore if it's not a file
|
|
54
|
+
|
|
55
|
+
def dropEvent(self, event):
|
|
56
|
+
"""Manage the drop event and retrieve the dropped file."""
|
|
57
|
+
if event.mimeData().hasUrls():
|
|
58
|
+
# Retrieve the URLs from the mimeData
|
|
59
|
+
urls = event.mimeData().urls()
|
|
60
|
+
# Convert the URLs to a local path
|
|
61
|
+
list_urls = []
|
|
62
|
+
for url in urls:
|
|
63
|
+
file_path = url.toLocalFile()
|
|
64
|
+
list_urls.append(file_path)
|
|
65
|
+
# Set the path in the QLineEdit
|
|
66
|
+
self.main_frame.old_save_input.setText(";".join(list_urls))
|
|
67
|
+
|
|
68
|
+
def check_for_updates(self):
|
|
69
|
+
"""Check for updates."""
|
|
70
|
+
repo_owner, repo_name = self.menu_bar.about_dialog.extract_repo_info(
|
|
71
|
+
metadata.url
|
|
72
|
+
)
|
|
73
|
+
updater = UpdateManager(repo_owner, repo_name, self)
|
|
74
|
+
reply = None
|
|
75
|
+
|
|
76
|
+
# Check for updates
|
|
77
|
+
if updater.check_updates() and self.yt.settings.auto_update:
|
|
78
|
+
msg = "An update is available,"
|
|
79
|
+
msg += "\nwould you like to update the software ?"
|
|
80
|
+
reply = QMessageBox.question(
|
|
81
|
+
None,
|
|
82
|
+
"Information",
|
|
83
|
+
msg,
|
|
84
|
+
QMessageBox.Yes | QMessageBox.No
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Download the update
|
|
88
|
+
if updater.check_updates() and reply == QMessageBox.Yes:
|
|
89
|
+
try:
|
|
90
|
+
updater.update_software()
|
|
91
|
+
updater.show_file_location_message(updater.new_filedir)
|
|
92
|
+
self.close()
|
|
93
|
+
sys.exit(self.qapp.exec())
|
|
94
|
+
except Exception as e:
|
|
95
|
+
msg = "An error occurred during the update"
|
|
96
|
+
run_error(msg, details = e)
|
|
97
|
+
|
ytComments/gui/error.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from PySide6.QtWidgets import (QApplication, QDialog, QLabel, QPushButton,
|
|
3
|
+
QVBoxLayout, QHBoxLayout, QSizePolicy, QWidget,
|
|
4
|
+
QMessageBox)
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
|
|
7
|
+
class ErrorWindow(QDialog):
|
|
8
|
+
def __init__(self, error_message:str = "Une erreur est survenue", error_details:str = ""):
|
|
9
|
+
super().__init__()
|
|
10
|
+
|
|
11
|
+
self.setWindowTitle("Erreur")
|
|
12
|
+
self.setModal(True)
|
|
13
|
+
self.layout = QVBoxLayout()
|
|
14
|
+
|
|
15
|
+
# Définir le drapeau pour rester au premier plan
|
|
16
|
+
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
17
|
+
|
|
18
|
+
# Icone d'erreur
|
|
19
|
+
warning_icon = QMessageBox().standardIcon(QMessageBox.Warning)
|
|
20
|
+
icon_label = QLabel()
|
|
21
|
+
icon_label.setPixmap(warning_icon)
|
|
22
|
+
icon_label.setAlignment(Qt.AlignLeft)
|
|
23
|
+
|
|
24
|
+
message_layout = QHBoxLayout()
|
|
25
|
+
message_layout.addWidget(icon_label)
|
|
26
|
+
|
|
27
|
+
# Message d'erreur simplifié
|
|
28
|
+
error_label = QLabel(error_message)
|
|
29
|
+
error_label.setAlignment(Qt.AlignLeft)
|
|
30
|
+
message_layout.addWidget(error_label)
|
|
31
|
+
self.layout.addLayout(message_layout)
|
|
32
|
+
|
|
33
|
+
# Bouton pour afficher le message d'erreur
|
|
34
|
+
self.toggle_button = QPushButton("Afficher le message d'erreur")
|
|
35
|
+
self.toggle_button.clicked.connect(self.toggle_message)
|
|
36
|
+
if error_details != "":
|
|
37
|
+
self.layout.addWidget(self.toggle_button)
|
|
38
|
+
|
|
39
|
+
# Message d'erreur détaillé
|
|
40
|
+
self.error_details_label = QLabel(error_details)
|
|
41
|
+
self.error_details_label.setVisible(False)
|
|
42
|
+
self.layout.addWidget(self.error_details_label)
|
|
43
|
+
|
|
44
|
+
# Permet de dimensionner en petit le bouton "OK"
|
|
45
|
+
button_layout = QHBoxLayout()
|
|
46
|
+
spacer = QWidget()
|
|
47
|
+
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
48
|
+
button_layout.addWidget(spacer)
|
|
49
|
+
|
|
50
|
+
# Bouton de fermeture
|
|
51
|
+
close_button = QPushButton("OK")
|
|
52
|
+
close_button.clicked.connect(self.close)
|
|
53
|
+
button_layout.addWidget(close_button)
|
|
54
|
+
self.layout.addLayout(button_layout)
|
|
55
|
+
|
|
56
|
+
self.setLayout(self.layout)
|
|
57
|
+
self.adjustSize()
|
|
58
|
+
|
|
59
|
+
# True = fenêtre détaillée, False = fenêtre simplifiée
|
|
60
|
+
self.expanded = False
|
|
61
|
+
|
|
62
|
+
def toggle_message(self):
|
|
63
|
+
"""Affiche ou masque le message d'erreur."""
|
|
64
|
+
|
|
65
|
+
if self.expanded:
|
|
66
|
+
self.toggle_button.setText("Afficher le message d'erreur")
|
|
67
|
+
self.error_details_label.setVisible(False)
|
|
68
|
+
self.adjustSize()
|
|
69
|
+
else:
|
|
70
|
+
self.toggle_button.setText("Masquer le message d'erreur")
|
|
71
|
+
self.error_details_label.setVisible(True)
|
|
72
|
+
self.adjustSize()
|
|
73
|
+
self.expanded = not self.expanded
|
|
74
|
+
|
|
75
|
+
def run_error(message:str = "", details:str = ""):
|
|
76
|
+
"""Lance la fenêtre d'erreur."""
|
|
77
|
+
app = QApplication.instance()
|
|
78
|
+
# Créez une nouvelle instance si nécessaire
|
|
79
|
+
if app is None:
|
|
80
|
+
app = QApplication(sys.argv)
|
|
81
|
+
|
|
82
|
+
window = ErrorWindow(str(message), str(details))
|
|
83
|
+
window.exec()
|
ytComments/gui/main.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from PySide6 import QtWidgets, QtCore
|
|
2
|
+
from .error import run_error
|
|
3
|
+
from ytComments.gui.progressbar import LoadingWindow
|
|
4
|
+
|
|
5
|
+
class MainWindow(QtWidgets.QFrame):
|
|
6
|
+
"""Main Window of the application."""
|
|
7
|
+
def __init__(self, parent: QtWidgets.QMainWindow):
|
|
8
|
+
super().__init__(parent)
|
|
9
|
+
self.app = parent
|
|
10
|
+
layout = QtWidgets.QVBoxLayout(self)
|
|
11
|
+
|
|
12
|
+
# Initialize the layout
|
|
13
|
+
layout.addLayout(self.init_url())
|
|
14
|
+
layout.addLayout(self.init_old_save())
|
|
15
|
+
layout.addLayout(self.init_new_save())
|
|
16
|
+
layout.addLayout(self.init_exe())
|
|
17
|
+
|
|
18
|
+
def init_url(self):
|
|
19
|
+
"""Initialize the URL."""
|
|
20
|
+
url_label = QtWidgets.QLabel("URL :")
|
|
21
|
+
self.url_input = QtWidgets.QLineEdit()
|
|
22
|
+
self.url_input.setText(self.app.yt.settings.channel_url)
|
|
23
|
+
layout = QtWidgets.QHBoxLayout()
|
|
24
|
+
layout.addWidget(url_label)
|
|
25
|
+
layout.addWidget(self.url_input)
|
|
26
|
+
return layout
|
|
27
|
+
|
|
28
|
+
def init_old_save(self):
|
|
29
|
+
"""Initialize the old save location."""
|
|
30
|
+
old_save_label = QtWidgets.QLabel("Old save :")
|
|
31
|
+
self.old_save_input = QtWidgets.QLineEdit()
|
|
32
|
+
msg = "More than one save can be specified by separating them with a semicolon"
|
|
33
|
+
self.old_save_input.setToolTip(msg)
|
|
34
|
+
old_save_button = QtWidgets.QPushButton("Browse")
|
|
35
|
+
old_save_button.clicked.connect(self.browse_file)
|
|
36
|
+
layout = QtWidgets.QHBoxLayout()
|
|
37
|
+
layout.addWidget(old_save_label)
|
|
38
|
+
layout.addWidget(self.old_save_input)
|
|
39
|
+
layout.addWidget(old_save_button)
|
|
40
|
+
return layout
|
|
41
|
+
|
|
42
|
+
def init_new_save(self):
|
|
43
|
+
"""Initialize the new save directory."""
|
|
44
|
+
directory_label = QtWidgets.QLabel("Save Directory :")
|
|
45
|
+
self.directory_input = QtWidgets.QLineEdit()
|
|
46
|
+
self.directory_input.setText(str(self.app.yt.settings.directory))
|
|
47
|
+
directory_button = QtWidgets.QPushButton("Browse")
|
|
48
|
+
directory_button.clicked.connect(self.browse_directory)
|
|
49
|
+
layout = QtWidgets.QHBoxLayout()
|
|
50
|
+
layout.addWidget(directory_label)
|
|
51
|
+
layout.addWidget(self.directory_input)
|
|
52
|
+
layout.addWidget(directory_button)
|
|
53
|
+
return layout
|
|
54
|
+
|
|
55
|
+
def init_exe(self):
|
|
56
|
+
"""Initialize the execute button."""
|
|
57
|
+
exe_button = QtWidgets.QPushButton("Execute")
|
|
58
|
+
exe_button.clicked.connect(self.execute)
|
|
59
|
+
layout = QtWidgets.QHBoxLayout()
|
|
60
|
+
layout.addWidget(exe_button)
|
|
61
|
+
return layout
|
|
62
|
+
|
|
63
|
+
def browse_file(self):
|
|
64
|
+
"""Open a file dialog to select files."""
|
|
65
|
+
files, _ = QtWidgets.QFileDialog.getOpenFileNames(
|
|
66
|
+
self, "Select the old saves", "", "Excel Files (*.xlsx)")
|
|
67
|
+
# Add them in the old save input
|
|
68
|
+
if files:
|
|
69
|
+
self.old_save_input.setText(";".join(files))
|
|
70
|
+
|
|
71
|
+
def browse_directory(self):
|
|
72
|
+
"""Open a file dialog to select a directory."""
|
|
73
|
+
directory = QtWidgets.QFileDialog.getExistingDirectory(
|
|
74
|
+
self, "Select a directory"
|
|
75
|
+
)
|
|
76
|
+
# Add them in the directory input
|
|
77
|
+
if directory:
|
|
78
|
+
self.directory_input.setText(directory)
|
|
79
|
+
self.app.yt.settings.directory = directory
|
|
80
|
+
self.app.yt.settings.save()
|
|
81
|
+
|
|
82
|
+
def execute(self):
|
|
83
|
+
"""Execute the program."""
|
|
84
|
+
self.app.yt.settings.channel_url = self.url_input.text()
|
|
85
|
+
old_saves = self.old_save_input.text().split(";")
|
|
86
|
+
directory = self.directory_input.text()
|
|
87
|
+
|
|
88
|
+
# Check the validity of the old saves
|
|
89
|
+
waste = ""
|
|
90
|
+
for save in old_saves.copy():
|
|
91
|
+
file = QtCore.QFile(save)
|
|
92
|
+
|
|
93
|
+
# If the file does not exist, remove it from the list
|
|
94
|
+
if not file.exists():
|
|
95
|
+
old_saves.remove(save)
|
|
96
|
+
waste += save
|
|
97
|
+
|
|
98
|
+
# Check if the waste is a valid file (eg: a file with ";" in its path)
|
|
99
|
+
file = QtCore.QFile(waste)
|
|
100
|
+
if file.exists():
|
|
101
|
+
old_saves.append(waste)
|
|
102
|
+
waste = ""
|
|
103
|
+
else:
|
|
104
|
+
waste += ";"
|
|
105
|
+
file = QtCore.QFile(waste)
|
|
106
|
+
if file.exists():
|
|
107
|
+
old_saves.append(waste)
|
|
108
|
+
waste = ""
|
|
109
|
+
|
|
110
|
+
# Check if the file is a .xlsx file
|
|
111
|
+
if file.exists() and QtCore.QFileInfo(file).suffix() != "xlsx":
|
|
112
|
+
run_error(
|
|
113
|
+
"Invalid file extension detected",
|
|
114
|
+
f"The file {file.fileName()} is not a .xlsx file"
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# If there isn't any old save, set it to None
|
|
119
|
+
if len(old_saves) == 0:
|
|
120
|
+
old_saves = None
|
|
121
|
+
else:
|
|
122
|
+
self.app.yt.old_save = old_saves
|
|
123
|
+
|
|
124
|
+
# Check if the directory exists
|
|
125
|
+
dir = QtCore.QFile(directory)
|
|
126
|
+
if not dir.exists():
|
|
127
|
+
run_error(
|
|
128
|
+
"Invalid directory",
|
|
129
|
+
f"The directory {directory} does not exist"
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
else:
|
|
133
|
+
self.app.yt.settings.directory = directory
|
|
134
|
+
|
|
135
|
+
# Save the settings
|
|
136
|
+
self.app.yt.settings.save()
|
|
137
|
+
|
|
138
|
+
# Reset the finish value
|
|
139
|
+
self.app.yt.finish = False
|
|
140
|
+
|
|
141
|
+
# Show the loading window
|
|
142
|
+
loading_window = LoadingWindow(countdown=True)
|
|
143
|
+
loading_window.show()
|
|
144
|
+
loading_window.update_progress(0)
|
|
145
|
+
|
|
146
|
+
# Launch the program
|
|
147
|
+
if old_saves is not None:
|
|
148
|
+
try:
|
|
149
|
+
self.app.yt.import_excel()
|
|
150
|
+
except:
|
|
151
|
+
run_error("Invalid save, choose a valid save")
|
|
152
|
+
loading_window.close()
|
|
153
|
+
return
|
|
154
|
+
self.app.yt.start() # Execute the thread
|
|
155
|
+
|
|
156
|
+
while self.app.yt.finish == False:
|
|
157
|
+
QtWidgets.QApplication.processEvents()
|
|
158
|
+
loading_window.update_progress(
|
|
159
|
+
self.app.yt.progress,
|
|
160
|
+
self.app.yt.duration
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Terminate the download earlier if the loading window is closed
|
|
164
|
+
if loading_window.closing:
|
|
165
|
+
self.app.yt.finish = True
|
|
166
|
+
|
|
167
|
+
loading_window.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|