ittup 1.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.
- ittup/__init__.py +1 -0
- ittup/__main__.py +5 -0
- ittup/common/__init__.py +4 -0
- ittup/common/about.py +15 -0
- ittup/common/bdinfo_string.py +98 -0
- ittup/common/bittorrent.py +14 -0
- ittup/common/command.py +162 -0
- ittup/common/constants.py +18 -0
- ittup/common/database.py +78 -0
- ittup/common/external_services/__init__.py +9 -0
- ittup/common/external_services/ftpx/__init__.py +0 -0
- ittup/common/external_services/ftpx/client.py +305 -0
- ittup/common/external_services/ftpx/core/__init__.py +0 -0
- ittup/common/external_services/ftpx/core/ftpx_service.py +76 -0
- ittup/common/external_services/ftpx/core/ftpx_session.py +159 -0
- ittup/common/external_services/ftpx/core/menu.py +17 -0
- ittup/common/external_services/ftpx/core/models/__init__.py +0 -0
- ittup/common/external_services/ftpx/core/models/list.py +17 -0
- ittup/common/external_services/igdb/__init__.py +0 -0
- ittup/common/external_services/igdb/client.py +262 -0
- ittup/common/external_services/igdb/core/__init__.py +0 -0
- ittup/common/external_services/igdb/core/api.py +70 -0
- ittup/common/external_services/igdb/core/models/__init__.py +0 -0
- ittup/common/external_services/igdb/core/models/search.py +13 -0
- ittup/common/external_services/igdb/core/platformid.py +24 -0
- ittup/common/external_services/igdb/core/tags.py +13 -0
- ittup/common/external_services/imageHost.py +324 -0
- ittup/common/external_services/mediaresult.py +25 -0
- ittup/common/external_services/sessions/__init__.py +0 -0
- ittup/common/external_services/sessions/agents.py +212 -0
- ittup/common/external_services/sessions/exceptions.py +100 -0
- ittup/common/external_services/sessions/session.py +149 -0
- ittup/common/external_services/theMovieDB/__init__.py +4 -0
- ittup/common/external_services/theMovieDB/core/__init__.py +8 -0
- ittup/common/external_services/theMovieDB/core/api.py +574 -0
- ittup/common/external_services/theMovieDB/core/keywords.py +7 -0
- ittup/common/external_services/theMovieDB/core/models/__init__.py +0 -0
- ittup/common/external_services/theMovieDB/core/models/movie/__init__.py +0 -0
- ittup/common/external_services/theMovieDB/core/models/movie/alternative.py +2 -0
- ittup/common/external_services/theMovieDB/core/models/movie/details.py +82 -0
- ittup/common/external_services/theMovieDB/core/models/movie/movie.py +48 -0
- ittup/common/external_services/theMovieDB/core/models/movie/nowplaying.py +69 -0
- ittup/common/external_services/theMovieDB/core/models/movie/release_info.py +49 -0
- ittup/common/external_services/theMovieDB/core/models/movie/translations.py +33 -0
- ittup/common/external_services/theMovieDB/core/models/tvshow/__init__.py +0 -0
- ittup/common/external_services/theMovieDB/core/models/tvshow/alternative.py +14 -0
- ittup/common/external_services/theMovieDB/core/models/tvshow/details.py +139 -0
- ittup/common/external_services/theMovieDB/core/models/tvshow/on_the_air.py +27 -0
- ittup/common/external_services/theMovieDB/core/models/tvshow/translations.py +45 -0
- ittup/common/external_services/theMovieDB/core/models/tvshow/tvshow.py +50 -0
- ittup/common/external_services/theMovieDB/core/videos.py +21 -0
- ittup/common/external_services/trailers/__init__.py +3 -0
- ittup/common/external_services/trailers/api.py +120 -0
- ittup/common/external_services/trailers/response.py +51 -0
- ittup/common/external_services/tvdb.py +45 -0
- ittup/common/extractor.py +124 -0
- ittup/common/frames.py +221 -0
- ittup/common/mediainfo.py +206 -0
- ittup/common/mediainfo_string.py +103 -0
- ittup/common/settings.py +952 -0
- ittup/common/tags.py +415 -0
- ittup/common/title.py +127 -0
- ittup/common/torrent_clients.py +280 -0
- ittup/common/trackers/__init__.py +8 -0
- ittup/common/trackers/ban_list.py +5 -0
- ittup/common/trackers/data.py +22 -0
- ittup/common/trackers/itt.py +153 -0
- ittup/common/trackers/signs_list.py +293 -0
- ittup/common/trackers/tags_list.py +118 -0
- ittup/common/trackers/trackers.py +46 -0
- ittup/common/utility.py +394 -0
- ittup/core/__init__.py +5 -0
- ittup/core/__main__.py +289 -0
- ittup/core/automode.py +122 -0
- ittup/core/bot.py +294 -0
- ittup/core/duplicate.py +270 -0
- ittup/core/exceptions.py +122 -0
- ittup/core/media.py +378 -0
- ittup/core/media_manager/ContentManager.py +199 -0
- ittup/core/media_manager/DocuManager.py +76 -0
- ittup/core/media_manager/GameManager.py +107 -0
- ittup/core/media_manager/MediaInfoManager.py +26 -0
- ittup/core/media_manager/SeedManager.py +49 -0
- ittup/core/media_manager/TorrentManager.py +153 -0
- ittup/core/media_manager/VideoManager.py +145 -0
- ittup/core/media_manager/__init__.py +0 -0
- ittup/core/media_manager/common.py +265 -0
- ittup/core/pvtDocu.py +123 -0
- ittup/core/pvtTorrent.py +78 -0
- ittup/core/pvtTracker.py +481 -0
- ittup/core/pvtVideo.py +104 -0
- ittup/core/torrent.py +467 -0
- ittup/core/upload.py +168 -0
- ittup/view/__init__.py +3 -0
- ittup/view/custom_console.py +148 -0
- ittup-1.1.0.dist-info/METADATA +182 -0
- ittup-1.1.0.dist-info/RECORD +101 -0
- ittup-1.1.0.dist-info/WHEEL +5 -0
- ittup-1.1.0.dist-info/entry_points.txt +2 -0
- ittup-1.1.0.dist-info/licenses/LICENSE +22 -0
- ittup-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from rich.prompt import Prompt
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from ittup.common.external_services.ftpx.core.models.list import FTPDirectory
|
|
9
|
+
from ittup.common.external_services.ftpx.core.ftpx_service import FtpX
|
|
10
|
+
from ittup.common import config_settings
|
|
11
|
+
|
|
12
|
+
from ittup.view import custom_console
|
|
13
|
+
|
|
14
|
+
class Folder:
|
|
15
|
+
"""
|
|
16
|
+
Represents a category with a name
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, name: str):
|
|
20
|
+
self.size: str | None = None
|
|
21
|
+
self.time: str | None = None
|
|
22
|
+
self.date: str | None = None
|
|
23
|
+
self.group: str | None = None
|
|
24
|
+
self.type: str | None = None
|
|
25
|
+
self.name: str = name
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MyPage:
|
|
29
|
+
"""
|
|
30
|
+
Handles pagination logic
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, items: list[Folder], items_per_page=50):
|
|
34
|
+
self.items = items
|
|
35
|
+
self.items_per_page = items_per_page
|
|
36
|
+
self.current_page = 1
|
|
37
|
+
self.total_pages = (
|
|
38
|
+
len(self.items) + self.items_per_page - 1
|
|
39
|
+
) // self.items_per_page
|
|
40
|
+
|
|
41
|
+
def get_items(self) -> list[Folder]:
|
|
42
|
+
"""
|
|
43
|
+
Returns the items for the current page
|
|
44
|
+
"""
|
|
45
|
+
start_index = (self.current_page - 1) * self.items_per_page
|
|
46
|
+
end_index = start_index + self.items_per_page
|
|
47
|
+
return self.items[start_index:end_index]
|
|
48
|
+
|
|
49
|
+
def home_page(self) -> None:
|
|
50
|
+
self.current_page = 1
|
|
51
|
+
|
|
52
|
+
def next_page(self) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Moves the 'cursor' to the next page
|
|
55
|
+
"""
|
|
56
|
+
if self.current_page < self.total_pages:
|
|
57
|
+
self.current_page += 1
|
|
58
|
+
|
|
59
|
+
def prev_page(self) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Moves the 'cursor' to the previous page
|
|
62
|
+
"""
|
|
63
|
+
if self.current_page > 1:
|
|
64
|
+
self.current_page -= 1
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def is_valid(selection: int, current_page_items: list[Folder]) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Validates the selected number
|
|
70
|
+
"""
|
|
71
|
+
return 1 <= selection <= len(current_page_items)
|
|
72
|
+
|
|
73
|
+
def get_absolute_index(self, selection: int) -> int:
|
|
74
|
+
"""
|
|
75
|
+
Returns the absolute index
|
|
76
|
+
"""
|
|
77
|
+
return (self.current_page - 1) * self.items_per_page + selection - 1
|
|
78
|
+
|
|
79
|
+
def build_page(self, page) -> Table:
|
|
80
|
+
"""
|
|
81
|
+
Builds a page (Table) with the current list of categories
|
|
82
|
+
"""
|
|
83
|
+
table = Table(
|
|
84
|
+
title=f"[bold magenta]Category Menu[/bold magenta] (Page {self.current_page}/{self.total_pages})",
|
|
85
|
+
style="dim",
|
|
86
|
+
title_style="bold yellow",
|
|
87
|
+
)
|
|
88
|
+
table.add_column(
|
|
89
|
+
"Index", style="cyan", header_style="bold cyan", justify="center", width=5
|
|
90
|
+
)
|
|
91
|
+
table.add_column("Category", style="green", header_style="bold green")
|
|
92
|
+
|
|
93
|
+
for idx, category in enumerate(
|
|
94
|
+
page, start=1
|
|
95
|
+
): # Adjust index to start from 1 for display
|
|
96
|
+
row_style = "white" if idx % 2 == 0 else "bright_black"
|
|
97
|
+
table.add_row(
|
|
98
|
+
f"[bold yellow]{idx}[/bold yellow]",
|
|
99
|
+
f"[italic green]{category.name}[/italic green]",
|
|
100
|
+
style=row_style,
|
|
101
|
+
)
|
|
102
|
+
return table
|
|
103
|
+
|
|
104
|
+
def select_category(self, action: str) -> Folder | None:
|
|
105
|
+
category_items_list = self.get_items()
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
selection = int(action)
|
|
109
|
+
if self.is_valid(selection, category_items_list):
|
|
110
|
+
# Get the absolute index
|
|
111
|
+
absolute_index = self.get_absolute_index(selection)
|
|
112
|
+
selected_category = self.items[absolute_index]
|
|
113
|
+
return selected_category
|
|
114
|
+
else:
|
|
115
|
+
custom_console.bot_error_log(
|
|
116
|
+
f"[red]Invalid number. Choose between 1 and {len(category_items_list)}[/red]"
|
|
117
|
+
)
|
|
118
|
+
except ValueError:
|
|
119
|
+
custom_console.bot_error_log(
|
|
120
|
+
"[red]Please enter a valid command or folder number[/red]"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Client:
|
|
127
|
+
"""
|
|
128
|
+
DATA
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self):
|
|
132
|
+
# New Ftp connection
|
|
133
|
+
self.ftpx_service = FtpX.new()
|
|
134
|
+
|
|
135
|
+
# // Build the first Home page
|
|
136
|
+
home_folder = self.ftpx_service.get_list(remote_path=config_settings.options.FTPX_ROOT)
|
|
137
|
+
|
|
138
|
+
# Quit if there are no files in the root folder
|
|
139
|
+
if not home_folder:
|
|
140
|
+
custom_console.bot_error_log(
|
|
141
|
+
"Root folder not found or there are no files.."
|
|
142
|
+
)
|
|
143
|
+
self.ftpx_service.quit()
|
|
144
|
+
|
|
145
|
+
# Create a page with the current folder
|
|
146
|
+
self.page = MyPage(home_folder)
|
|
147
|
+
# current ftp remote path
|
|
148
|
+
self.remote_path = None
|
|
149
|
+
# current download local path
|
|
150
|
+
self.download_to_local_path = None
|
|
151
|
+
# Current list of files (as shown in the Table of Contents)
|
|
152
|
+
self.current_list_of_files: list["FTPDirectory"] = []
|
|
153
|
+
# Selected single file
|
|
154
|
+
self.single_file_selected: bool = False
|
|
155
|
+
|
|
156
|
+
def home_page(self):
|
|
157
|
+
# Build the current Home page
|
|
158
|
+
self.page.home_page()
|
|
159
|
+
# Get a list of items
|
|
160
|
+
page_items_list = self.page.get_items()
|
|
161
|
+
# Build and fill the table
|
|
162
|
+
home_table = self.page.build_page(page_items_list)
|
|
163
|
+
# return the data
|
|
164
|
+
return home_table
|
|
165
|
+
|
|
166
|
+
def page_next(self):
|
|
167
|
+
# Set the 'program counter...' to next page
|
|
168
|
+
self.page.next_page()
|
|
169
|
+
# Get a list of items
|
|
170
|
+
page_items_list = self.page.get_items()
|
|
171
|
+
# return the built data table
|
|
172
|
+
return self.page.build_page(page_items_list)
|
|
173
|
+
|
|
174
|
+
def page_prev(self):
|
|
175
|
+
# Set the 'program counter...' to prev page
|
|
176
|
+
self.page.prev_page()
|
|
177
|
+
# Get a list of items
|
|
178
|
+
page_items_list = self.page.get_items()
|
|
179
|
+
# return the built data table
|
|
180
|
+
return self.page.build_page(page_items_list)
|
|
181
|
+
|
|
182
|
+
def page_up(self):
|
|
183
|
+
# Deselect any previously selected single file
|
|
184
|
+
self.single_file_selected = False
|
|
185
|
+
# get the previous path
|
|
186
|
+
split_path = [part for part in self.remote_path.split("/") if part]
|
|
187
|
+
# build the new path
|
|
188
|
+
up_path = f"/{'/'.join(split_path[:-1])}"
|
|
189
|
+
# return the built data table
|
|
190
|
+
return self.change_path(selected_folder=up_path)
|
|
191
|
+
|
|
192
|
+
def download(self):
|
|
193
|
+
|
|
194
|
+
if not self.single_file_selected:
|
|
195
|
+
# Get the download list
|
|
196
|
+
download_list = [
|
|
197
|
+
(os.path.join(self.remote_path, file.name), file.size)
|
|
198
|
+
for file in self.current_list_of_files
|
|
199
|
+
]
|
|
200
|
+
else:
|
|
201
|
+
# Get the single file selected
|
|
202
|
+
download_list = [(self.remote_path, self.current_list_of_files[0].size)]
|
|
203
|
+
|
|
204
|
+
for remote_file, size in download_list:
|
|
205
|
+
|
|
206
|
+
# format the path. Replace '\' to '/' from Windows OS to linux
|
|
207
|
+
remote_file = remote_file.replace("\\", "/")
|
|
208
|
+
|
|
209
|
+
# Skip the first '/' otherwise it would create a list with a leading space
|
|
210
|
+
remote_file_path = remote_file[1:].split("/")
|
|
211
|
+
# Get only the last two subfolders
|
|
212
|
+
if len(remote_file_path) > 2:
|
|
213
|
+
remote_short_path = remote_file_path[-2:]
|
|
214
|
+
else:
|
|
215
|
+
remote_short_path = remote_file_path
|
|
216
|
+
|
|
217
|
+
self.download_to_local_path = os.path.join(
|
|
218
|
+
config_settings.options.FTPX_LOCAL_PATH, *remote_short_path
|
|
219
|
+
)
|
|
220
|
+
custom_console.bot_log(
|
|
221
|
+
f"Server:{os.path.basename(remote_file)} -> Client:{self.download_to_local_path}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self.ftpx_service.download_file(
|
|
225
|
+
remote_path=remote_file, local_path=self.download_to_local_path
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def select_file(self, one_file_selected: FTPDirectory):
|
|
229
|
+
# Update the remote path
|
|
230
|
+
self.remote_path = os.path.join(
|
|
231
|
+
self.ftpx_service.current_path(), one_file_selected.name
|
|
232
|
+
)
|
|
233
|
+
self.single_file_selected = True
|
|
234
|
+
# format the path. Replace '\' to '/' from Windows OS to linux
|
|
235
|
+
self.remote_path = self.remote_path.replace("\\", "/")
|
|
236
|
+
|
|
237
|
+
# Create a list of FPTDirectory for a single file
|
|
238
|
+
self.current_list_of_files = [one_file_selected]
|
|
239
|
+
|
|
240
|
+
def change_path(self, selected_folder: str):
|
|
241
|
+
self.remote_path = os.path.join(
|
|
242
|
+
self.ftpx_service.current_path(), selected_folder
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# format the path. Replace '\' to '/' from Windows OS to linux
|
|
246
|
+
self.remote_path = self.remote_path.replace("\\", "/")
|
|
247
|
+
self.ftpx_service.change_dir(new_path=self.remote_path)
|
|
248
|
+
# // Build a new Home page for the current folder
|
|
249
|
+
home_folder = self.ftpx_service.get_list(remote_path=config_settings.options.FTPX_ROOT)
|
|
250
|
+
# Create a page with the current folder
|
|
251
|
+
self.page = MyPage(home_folder)
|
|
252
|
+
# Save the current list of files
|
|
253
|
+
self.current_list_of_files = home_folder
|
|
254
|
+
return self.home_page()
|
|
255
|
+
|
|
256
|
+
def user_input_search(self):
|
|
257
|
+
custom_console.print("Search '->' ", end='', style='violet bold')
|
|
258
|
+
keyword = input()
|
|
259
|
+
for item in self.page.get_items():
|
|
260
|
+
if keyword.lower() in item.name.lower():
|
|
261
|
+
custom_console.bot_question_log(
|
|
262
|
+
f"\nTYPE: {item.type} \nNAME: {item.name} \nGROUP: {item.group} \nSIZE: {item.size}"
|
|
263
|
+
f"DATE: {item.date}\n TIME: {item.time}\n")
|
|
264
|
+
custom_console.rule(style='violet bold')
|
|
265
|
+
|
|
266
|
+
def search(self):
|
|
267
|
+
self.user_input_search()
|
|
268
|
+
|
|
269
|
+
def user_input(self) -> str:
|
|
270
|
+
"""
|
|
271
|
+
Asks the user for the folder.
|
|
272
|
+
"""
|
|
273
|
+
if not self.remote_path:
|
|
274
|
+
self.remote_path = "/"
|
|
275
|
+
|
|
276
|
+
prompt_message = Text(f"\n-> {self.remote_path}\n", style="bold violet")
|
|
277
|
+
|
|
278
|
+
prompt_message.append(
|
|
279
|
+
"[N]next, [P]rev, [U]p, [D]ownload, [Q]uit, [S]earch or enter a valid number",
|
|
280
|
+
style="reset",
|
|
281
|
+
)
|
|
282
|
+
return Prompt.ask(prompt_message)
|
|
283
|
+
|
|
284
|
+
def input_manager(self, action: str):
|
|
285
|
+
if action.upper() == "N":
|
|
286
|
+
return self.page_next()
|
|
287
|
+
elif action.upper() == "P":
|
|
288
|
+
return self.page_prev()
|
|
289
|
+
elif action.upper() == "U":
|
|
290
|
+
return self.page_up()
|
|
291
|
+
elif action.upper() == "D":
|
|
292
|
+
self.download()
|
|
293
|
+
return 0
|
|
294
|
+
elif action.upper() == "S":
|
|
295
|
+
self.search()
|
|
296
|
+
elif action.upper() == "Q":
|
|
297
|
+
return 0
|
|
298
|
+
else:
|
|
299
|
+
return self.page.select_category(action)
|
|
300
|
+
|
|
301
|
+
def quit(self):
|
|
302
|
+
self.ftpx_service.quit()
|
|
303
|
+
|
|
304
|
+
def sys_info(self):
|
|
305
|
+
return self.ftpx_service.syst()
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from ittup.common.external_services.ftpx.core.ftpx_session import FtpXCmds
|
|
6
|
+
from ittup.common.external_services.ftpx.core.models.list import FTPDirectory
|
|
7
|
+
from ittup.common.utility import MyString
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FtpX(FtpXCmds):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__()
|
|
13
|
+
|
|
14
|
+
def get_list(self, remote_path=".") -> ["FTPDirectory"]:
|
|
15
|
+
# Get a list from the FTP server
|
|
16
|
+
file_list = self._list(path=remote_path)
|
|
17
|
+
if not file_list:
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
folder = [
|
|
21
|
+
FTPDirectory(
|
|
22
|
+
permissions=parts[0],
|
|
23
|
+
type="Folder" if parts[0][:1] == "d" else "File",
|
|
24
|
+
links=int(parts[1]),
|
|
25
|
+
owner=parts[2],
|
|
26
|
+
group=parts[3],
|
|
27
|
+
size=int(parts[4]),
|
|
28
|
+
date=date_time.date(),
|
|
29
|
+
time=date_time.time(),
|
|
30
|
+
name=parts[8],
|
|
31
|
+
)
|
|
32
|
+
for file in file_list
|
|
33
|
+
if (parts := file.split()) and "total" not in parts
|
|
34
|
+
if (
|
|
35
|
+
date_time := MyString.parse_date(file)
|
|
36
|
+
) # Assign date_time only if its valid
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# The first in list is the most recent
|
|
40
|
+
sorted_folder = sorted(
|
|
41
|
+
folder,
|
|
42
|
+
key=lambda entry: datetime.combine(entry.date, entry.time),
|
|
43
|
+
reverse=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return sorted_folder
|
|
47
|
+
|
|
48
|
+
def current_path(self):
|
|
49
|
+
return self._pwd()
|
|
50
|
+
|
|
51
|
+
def change_dir(self, new_path: str):
|
|
52
|
+
return self.cwd(dirname=new_path)
|
|
53
|
+
|
|
54
|
+
def download_file(self, remote_path: str, local_path: str):
|
|
55
|
+
"""Download a file from the ftp server"""
|
|
56
|
+
|
|
57
|
+
# format the path. Replace '\' to '/' from Windows OS to linux
|
|
58
|
+
remote_path = remote_path.replace("\\", "/")
|
|
59
|
+
|
|
60
|
+
# Create the local folder if it does not exist
|
|
61
|
+
local_dir = os.path.dirname(local_path)
|
|
62
|
+
if not os.path.exists(local_dir):
|
|
63
|
+
os.makedirs(local_dir)
|
|
64
|
+
|
|
65
|
+
# Download...
|
|
66
|
+
self._retr(
|
|
67
|
+
remote_path=remote_path,
|
|
68
|
+
local_path=local_path,
|
|
69
|
+
size=self.file_size(remote_path),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def file_size(self, file_path: str):
|
|
73
|
+
return self._size(file_path=file_path)
|
|
74
|
+
|
|
75
|
+
def syst(self):
|
|
76
|
+
return self._syst()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import ftplib
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from rich.progress import Progress
|
|
6
|
+
|
|
7
|
+
from ftplib import FTP_TLS, all_errors
|
|
8
|
+
from ittup.common import config_settings
|
|
9
|
+
from ittup.view import custom_console
|
|
10
|
+
|
|
11
|
+
class FtpXCmds(FTP_TLS):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.is_quitting = False
|
|
15
|
+
if config_settings.options.FTPX_KEEP_ALIVE:
|
|
16
|
+
# Time interval to keep the connection alive (in seconds)
|
|
17
|
+
self.keep_alive_interval = 10
|
|
18
|
+
# flag to stop the keep-alive thread
|
|
19
|
+
self.keep_alive_stop_flag = threading.Event()
|
|
20
|
+
# Separate thread to run noop to the server
|
|
21
|
+
self.keep_alive_thread = threading.Thread(target=self._keep_alive)
|
|
22
|
+
# Daemon mode
|
|
23
|
+
self.keep_alive_thread.daemon = True
|
|
24
|
+
self.keep_alive_thread.start()
|
|
25
|
+
|
|
26
|
+
def _keep_alive(self):
|
|
27
|
+
while not self.keep_alive_stop_flag.is_set():
|
|
28
|
+
time.sleep(self.keep_alive_interval)
|
|
29
|
+
try:
|
|
30
|
+
self.voidcmd("NOOP")
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"Error during keep-alive: {e}")
|
|
33
|
+
if not self.is_quitting:
|
|
34
|
+
self.quit()
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _execute_command(command_fn, *args, **kwargs):
|
|
38
|
+
try:
|
|
39
|
+
return command_fn(*args, **kwargs)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
custom_console.bot_error_log(
|
|
42
|
+
f"Error during {command_fn.__name__} command: {e}"
|
|
43
|
+
)
|
|
44
|
+
exit(1)
|
|
45
|
+
|
|
46
|
+
def quit(self):
|
|
47
|
+
custom_console.bot_question_log("Exiting... Please wait for the app to close\n")
|
|
48
|
+
if self.is_quitting:
|
|
49
|
+
return
|
|
50
|
+
if config_settings.options.FTPX_KEEP_ALIVE:
|
|
51
|
+
# Set the stop flag for the keep-alive thread
|
|
52
|
+
self.keep_alive_stop_flag.set()
|
|
53
|
+
if self.keep_alive_thread.is_alive():
|
|
54
|
+
# Wait for the thread to finish
|
|
55
|
+
self.keep_alive_thread.join()
|
|
56
|
+
self.is_quitting = True
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
super().quit()
|
|
60
|
+
except ftplib.error_temp as e:
|
|
61
|
+
custom_console.bot_error_log(e)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def new(
|
|
65
|
+
cls,
|
|
66
|
+
host=config_settings.options.FTPX_IP,
|
|
67
|
+
port=int(config_settings.options.FTPX_PORT),
|
|
68
|
+
user=config_settings.options.FTPX_USER,
|
|
69
|
+
passwd=config_settings.options.FTPX_PASS,
|
|
70
|
+
):
|
|
71
|
+
|
|
72
|
+
validate_ftpx_config = True
|
|
73
|
+
if not config_settings.options.FTPX_IP:
|
|
74
|
+
custom_console.bot_question_log("No FTPX_IP provided\n")
|
|
75
|
+
validate_ftpx_config = False
|
|
76
|
+
|
|
77
|
+
if not config_settings.options.FTPX_PORT:
|
|
78
|
+
custom_console.bot_question_log("No FTPX_PORT provided\n")
|
|
79
|
+
validate_ftpx_config = False
|
|
80
|
+
|
|
81
|
+
if not config_settings.options.FTPX_USER:
|
|
82
|
+
custom_console.bot_question_log("No FTPX_USER provided\n")
|
|
83
|
+
validate_ftpx_config = False
|
|
84
|
+
|
|
85
|
+
if not config_settings.options.FTPX_PASS:
|
|
86
|
+
custom_console.bot_question_log("No FTPX_PASS provided\n")
|
|
87
|
+
validate_ftpx_config = False
|
|
88
|
+
|
|
89
|
+
if not config_settings.options.FTPX_LOCAL_PATH:
|
|
90
|
+
custom_console.bot_question_log("No FTPX_LOCAL_PATH provided\n")
|
|
91
|
+
validate_ftpx_config = False
|
|
92
|
+
|
|
93
|
+
if not validate_ftpx_config:
|
|
94
|
+
custom_console.bot_error_log(f"Please check your config file or verify if the FTP server is online")
|
|
95
|
+
exit(1)
|
|
96
|
+
|
|
97
|
+
"""Create an instance of FtpXCmds and handle connection and login"""
|
|
98
|
+
ftp = cls()
|
|
99
|
+
try:
|
|
100
|
+
ftp.connect(host, port)
|
|
101
|
+
ftp.login(user=user, passwd=passwd)
|
|
102
|
+
ftp.prot_p() # Enable SSL
|
|
103
|
+
except all_errors as e:
|
|
104
|
+
custom_console.bot_error_log(f"\nFTP Server Error: {e}")
|
|
105
|
+
custom_console.bot_error_log(
|
|
106
|
+
f"Please check your config file or verify if the FTP server is online"
|
|
107
|
+
)
|
|
108
|
+
exit(1)
|
|
109
|
+
return ftp
|
|
110
|
+
|
|
111
|
+
def _send_pret(self, command):
|
|
112
|
+
"""Send PRET command to the FTP server"""
|
|
113
|
+
return self.sendcmd(f"PRET {command}")
|
|
114
|
+
|
|
115
|
+
def _list(self, path="") -> list:
|
|
116
|
+
"""Run LIST with PRET and return list of files and directories"""
|
|
117
|
+
output_lines = []
|
|
118
|
+
|
|
119
|
+
def collect_line(line):
|
|
120
|
+
output_lines.append(line)
|
|
121
|
+
|
|
122
|
+
self._execute_command(self._send_pret, "LIST")
|
|
123
|
+
self._execute_command(self.retrlines, f"LIST {path}", callback=collect_line)
|
|
124
|
+
return output_lines
|
|
125
|
+
|
|
126
|
+
def _retr(self, remote_path, local_path: str, size: int) -> bool:
|
|
127
|
+
def download():
|
|
128
|
+
with open(local_path, "wb") as local_file:
|
|
129
|
+
with Progress() as progress:
|
|
130
|
+
task = progress.add_task(
|
|
131
|
+
"[cyan]Downloading...", total=size, visible=True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def write_chunk(chunk):
|
|
135
|
+
local_file.write(chunk)
|
|
136
|
+
progress.update(task, advance=len(chunk))
|
|
137
|
+
|
|
138
|
+
self.retrbinary(f"RETR {remote_path}", write_chunk)
|
|
139
|
+
|
|
140
|
+
return self._execute_command(
|
|
141
|
+
self._send_pret, f"RETR {remote_path}"
|
|
142
|
+
) and self._execute_command(download)
|
|
143
|
+
|
|
144
|
+
def _cwd(self, path):
|
|
145
|
+
# Change the working directory on the FTP server using PRET
|
|
146
|
+
self._execute_command(self._send_pret, f"CWD {path}")
|
|
147
|
+
self._execute_command(self.cwd, path)
|
|
148
|
+
|
|
149
|
+
def _pwd(self) -> str:
|
|
150
|
+
# Get the current directory on the FTP server
|
|
151
|
+
return self._execute_command(self.pwd)
|
|
152
|
+
|
|
153
|
+
def _size(self, file_path: str):
|
|
154
|
+
# Get the size of a file on the FTP server
|
|
155
|
+
return self._execute_command(self.size, filename=file_path)
|
|
156
|
+
|
|
157
|
+
def _syst(self):
|
|
158
|
+
# Send the syst command to get system info from the server
|
|
159
|
+
return self._execute_command(self.sendcmd, "SYST")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Menu:
|
|
5
|
+
"""
|
|
6
|
+
Create a new Menu.
|
|
7
|
+
VIEW
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.console = Console()
|
|
12
|
+
|
|
13
|
+
def show(self, table) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Displays the current page.
|
|
16
|
+
"""
|
|
17
|
+
self.console.print(table)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class FTPDirectory:
|
|
9
|
+
permissions: str | None = None
|
|
10
|
+
type: str | None = None
|
|
11
|
+
links: int | None = None
|
|
12
|
+
owner: str | None = None
|
|
13
|
+
group: str | None = None
|
|
14
|
+
size: int | None = None
|
|
15
|
+
date: datetime.date | None = None
|
|
16
|
+
time: datetime.time | None = None
|
|
17
|
+
name: str | None = None
|
|
File without changes
|