src/eric7/WebBrowser/History/HistoryManager.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9153
506e35e424d5
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the history manager.
8 """
9
10 import os
11 import pathlib
12
13 from PyQt6.QtCore import (
14 pyqtSignal, pyqtSlot, QDateTime, QDate, QTime, QUrl, QTimer, QFile,
15 QIODevice, QByteArray, QDataStream, QTemporaryFile, QObject
16 )
17
18 from EricWidgets import EricMessageBox
19
20 from Utilities.AutoSaver import AutoSaver
21 import Utilities
22 import Preferences
23
24 HISTORY_VERSION_42 = 42
25 HISTORY_VERSION_60 = 60
26 HISTORY_VERSIONS = [HISTORY_VERSION_60, HISTORY_VERSION_42]
27
28
29 class HistoryEntry:
30 """
31 Class implementing a history entry.
32 """
33 def __init__(self, url=None, dateTime=None, title=None, visitCount=None):
34 """
35 Constructor
36
37 @param url URL of the history entry (string)
38 @param dateTime date and time this entry was created (QDateTime)
39 @param title title string for the history entry (string)
40 @param visitCount number of visits of this URL (int)
41 """
42 self.url = url and url or ""
43 self.dateTime = dateTime and dateTime or QDateTime()
44 self.title = title and title or ""
45 self.visitCount = visitCount and visitCount or 0
46
47 def __eq__(self, other):
48 """
49 Special method determining equality.
50
51 @param other reference to the history entry to compare against
52 (HistoryEntry)
53 @return flag indicating equality (boolean)
54 """
55 return (
56 other.title == self.title and
57 other.url == self.url and
58 other.dateTime == self.dateTime
59 )
60
61 def __lt__(self, other):
62 """
63 Special method determining less relation.
64
65 Note: History is sorted in reverse order by date and time
66
67 @param other reference to the history entry to compare against
68 (HistoryEntry)
69 @return flag indicating less (boolean)
70 """
71 return self.dateTime > other.dateTime
72
73 def userTitle(self):
74 """
75 Public method to get the title of the history entry.
76
77 @return title of the entry (string)
78 """
79 if not self.title:
80 page = pathlib.Path(QUrl(self.url).path()).name
81 if page:
82 return page
83 return self.url
84 return self.title
85
86 def isValid(self):
87 """
88 Public method to determine validity.
89
90 @return flag indicating validity
91 @rtype bool
92 """
93 return bool(self.url) and self.dateTime.isValid()
94
95
96 class HistoryManager(QObject):
97 """
98 Class implementing the history manager.
99
100 @signal historyCleared() emitted after the history has been cleared
101 @signal historyReset() emitted after the history has been reset
102 @signal entryAdded(HistoryEntry) emitted after a history entry has been
103 added
104 @signal entryRemoved(HistoryEntry) emitted after a history entry has been
105 removed
106 @signal entryUpdated(int) emitted after a history entry has been updated
107 @signal historySaved() emitted after the history was saved
108 """
109 historyCleared = pyqtSignal()
110 historyReset = pyqtSignal()
111 entryAdded = pyqtSignal(HistoryEntry)
112 entryRemoved = pyqtSignal(HistoryEntry)
113 entryUpdated = pyqtSignal(int)
114 historySaved = pyqtSignal()
115
116 def __init__(self, parent=None):
117 """
118 Constructor
119
120 @param parent reference to the parent object (QObject)
121 """
122 super().__init__(parent)
123
124 self.__saveTimer = AutoSaver(self, self.save)
125 self.__daysToExpire = Preferences.getWebBrowser("HistoryLimit")
126 self.__history = []
127 self.__lastSavedUrl = ""
128
129 self.__expiredTimer = QTimer(self)
130 self.__expiredTimer.setSingleShot(True)
131 self.__expiredTimer.timeout.connect(self.__checkForExpired)
132
133 self.__frequencyTimer = QTimer(self)
134 self.__frequencyTimer.setSingleShot(True)
135 self.__frequencyTimer.timeout.connect(self.__refreshFrequencies)
136
137 self.entryAdded.connect(self.__saveTimer.changeOccurred)
138 self.entryRemoved.connect(self.__saveTimer.changeOccurred)
139
140 self.__load()
141
142 from .HistoryModel import HistoryModel
143 from .HistoryFilterModel import HistoryFilterModel
144 from .HistoryTreeModel import HistoryTreeModel
145
146 self.__historyModel = HistoryModel(self, self)
147 self.__historyFilterModel = HistoryFilterModel(
148 self.__historyModel, self)
149 self.__historyTreeModel = HistoryTreeModel(
150 self.__historyFilterModel, self)
151
152 self.__startFrequencyTimer()
153
154 def close(self):
155 """
156 Public method to close the history manager.
157 """
158 # remove history items on application exit
159 if self.__daysToExpire == -2:
160 self.clear()
161 self.__saveTimer.saveIfNeccessary()
162
163 def history(self):
164 """
165 Public method to return the history.
166
167 @return reference to the list of history entries (list of HistoryEntry)
168 """
169 return self.__history[:]
170
171 def setHistory(self, history, loadedAndSorted=False):
172 """
173 Public method to set a new history.
174
175 @param history reference to the list of history entries to be set
176 (list of HistoryEntry)
177 @param loadedAndSorted flag indicating that the list is sorted
178 (boolean)
179 """
180 self.__history = history[:]
181 if not loadedAndSorted:
182 self.__history.sort()
183
184 self.__checkForExpired()
185
186 if loadedAndSorted:
187 try:
188 self.__lastSavedUrl = self.__history[0].url
189 except IndexError:
190 self.__lastSavedUrl = ""
191 else:
192 self.__lastSavedUrl = ""
193 self.__saveTimer.changeOccurred()
194 self.historyReset.emit()
195
196 def __findFirstHistoryEntry(self, url):
197 """
198 Private method to find the first entry for the given URL.
199
200 @param url URL to search for
201 @type str
202 @return first entry for the given URL
203 @rtype HistoryEntry
204 """
205 for index in range(len(self.__history)):
206 if url == self.__history[index].url:
207 return self.__history[index]
208
209 # not found, return an empty entry
210 return HistoryEntry()
211
212 def __updateVisitCount(self, url, count):
213 """
214 Private method to update the visit count for all entries of the
215 given URL.
216
217 @param url URL to be updated
218 @type str
219 @param count new visit count
220 @type int
221 """
222 for index in range(len(self.__history)):
223 if url == self.__history[index].url:
224 self.__history[index].visitCount = count
225
226 def addHistoryEntry(self, view):
227 """
228 Public method to add a history entry.
229
230 @param view reference to the view to add an entry for
231 @type WebBrowserView
232 """
233 import WebBrowser.WebBrowserWindow
234 if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate():
235 return
236
237 url = view.url()
238 title = view.title()
239
240 if url.scheme() not in ["eric", "about", "data", "chrome"]:
241 cleanUrlStr = self.__cleanUrlStr(url)
242 firstEntry = self.__findFirstHistoryEntry(cleanUrlStr)
243 if firstEntry.isValid():
244 visitCount = firstEntry.visitCount + 1
245 self.__updateVisitCount(cleanUrlStr, visitCount)
246 else:
247 visitCount = 1
248 itm = HistoryEntry(cleanUrlStr,
249 QDateTime.currentDateTime(),
250 title,
251 visitCount)
252 self.__history.insert(0, itm)
253 self.entryAdded.emit(itm)
254 if len(self.__history) == 1:
255 self.__checkForExpired()
256
257 def updateHistoryEntry(self, url, title):
258 """
259 Public method to update a history entry.
260
261 @param url URL of the entry to update (string)
262 @param title title of the entry to update (string)
263 """
264 if QUrl(url).scheme() not in ["eric", "about", "data", "chrome"]:
265 cleanUrlStr = self.__cleanUrlStr(QUrl(url))
266 for index in range(len(self.__history)):
267 if cleanUrlStr == self.__history[index].url:
268 self.__history[index].title = title
269 self.__saveTimer.changeOccurred()
270 if not self.__lastSavedUrl:
271 self.__lastSavedUrl = self.__history[index].url
272 self.entryUpdated.emit(index)
273 break
274
275 def removeHistoryEntry(self, url, title=""):
276 """
277 Public method to remove a history entry.
278
279 @param url URL of the entry to remove (QUrl)
280 @param title title of the entry to remove (string)
281 """
282 if url.scheme() not in ["eric", "about", "data", "chrome"]:
283 cleanUrlStr = self.__cleanUrlStr(url)
284 for index in range(len(self.__history)):
285 if (
286 cleanUrlStr == self.__history[index].url and
287 (not title or title == self.__history[index].title)
288 ):
289 itm = self.__history[index]
290 self.__lastSavedUrl = ""
291 self.__history.remove(itm)
292 self.entryRemoved.emit(itm)
293 break
294
295 def __cleanUrl(self, url):
296 """
297 Private method to generate a clean URL usable for the history entry.
298
299 @param url original URL
300 @type QUrl
301 @return cleaned URL
302 @rtype QUrl
303 """
304 cleanurl = QUrl(url)
305 if cleanurl.password():
306 # don't save the password in the history
307 cleanurl.setPassword("")
308 if cleanurl.host():
309 # convert host to lower case
310 cleanurl.setHost(url.host().lower())
311
312 return cleanurl
313
314 def __cleanUrlStr(self, url):
315 """
316 Private method to generate a clean URL usable for the history entry.
317
318 @param url original URL
319 @type QUrl
320 @return cleaned URL
321 @rtype str
322 """
323 cleanurl = self.__cleanUrl(url)
324 return cleanurl.toString()
325
326 def historyModel(self):
327 """
328 Public method to get a reference to the history model.
329
330 @return reference to the history model (HistoryModel)
331 """
332 return self.__historyModel
333
334 def historyFilterModel(self):
335 """
336 Public method to get a reference to the history filter model.
337
338 @return reference to the history filter model (HistoryFilterModel)
339 """
340 return self.__historyFilterModel
341
342 def historyTreeModel(self):
343 """
344 Public method to get a reference to the history tree model.
345
346 @return reference to the history tree model (HistoryTreeModel)
347 """
348 return self.__historyTreeModel
349
350 def __checkForExpired(self):
351 """
352 Private slot to check entries for expiration.
353 """
354 if self.__daysToExpire < 0 or len(self.__history) == 0:
355 return
356
357 now = QDateTime.currentDateTime()
358 nextTimeout = 0
359
360 while self.__history:
361 checkForExpired = QDateTime(self.__history[-1].dateTime)
362 checkForExpired.setDate(
363 checkForExpired.date().addDays(self.__daysToExpire))
364 nextTimeout = (
365 7 * 86400
366 if now.daysTo(checkForExpired) > 7 else
367 now.secsTo(checkForExpired)
368 )
369 if nextTimeout > 0:
370 break
371
372 itm = self.__history.pop(-1)
373 self.__lastSavedUrl = ""
374 self.entryRemoved.emit(itm)
375 self.__saveTimer.saveIfNeccessary()
376
377 if nextTimeout > 0:
378 self.__expiredTimer.start(nextTimeout * 1000)
379
380 def daysToExpire(self):
381 """
382 Public method to get the days for entry expiration.
383
384 @return days for entry expiration (integer)
385 """
386 return self.__daysToExpire
387
388 def setDaysToExpire(self, limit):
389 """
390 Public method to set the days for entry expiration.
391
392 @param limit days for entry expiration (integer)
393 """
394 if self.__daysToExpire == limit:
395 return
396
397 self.__daysToExpire = limit
398 self.__checkForExpired()
399 self.__saveTimer.changeOccurred()
400
401 def preferencesChanged(self):
402 """
403 Public method to indicate a change of preferences.
404 """
405 self.setDaysToExpire(Preferences.getWebBrowser("HistoryLimit"))
406
407 @pyqtSlot()
408 def clear(self, period=0):
409 """
410 Public slot to clear the complete history.
411
412 @param period history period in milliseconds to be cleared (integer)
413 """
414 if period == 0:
415 self.__history = []
416 self.historyReset.emit()
417 else:
418 breakMS = QDateTime.currentMSecsSinceEpoch() - period
419 while (
420 self.__history and
421 (QDateTime(self.__history[0].dateTime).toMSecsSinceEpoch() >
422 breakMS)
423 ):
424 itm = self.__history.pop(0)
425 self.entryRemoved.emit(itm)
426 self.__lastSavedUrl = ""
427 self.__saveTimer.changeOccurred()
428 self.__saveTimer.saveIfNeccessary()
429 self.historyCleared.emit()
430
431 def getFileName(self):
432 """
433 Public method to get the file name of the history file.
434
435 @return name of the history file (string)
436 """
437 return os.path.join(Utilities.getConfigDir(), "web_browser", "history")
438
439 def reload(self):
440 """
441 Public method to reload the history.
442 """
443 self.__load()
444
445 def __load(self):
446 """
447 Private method to load the saved history entries from disk.
448 """
449 historyFile = QFile(self.getFileName())
450 if not historyFile.exists():
451 return
452 if not historyFile.open(QIODevice.OpenModeFlag.ReadOnly):
453 EricMessageBox.warning(
454 None,
455 self.tr("Loading History"),
456 self.tr(
457 """<p>Unable to open history file <b>{0}</b>.<br/>"""
458 """Reason: {1}</p>""")
459 .format(historyFile.fileName, historyFile.errorString()))
460 return
461
462 history = []
463
464 # double check, that the history file is sorted as it is read
465 needToSort = False
466 lastInsertedItem = HistoryEntry()
467 data = QByteArray(historyFile.readAll())
468 stream = QDataStream(data, QIODevice.OpenModeFlag.ReadOnly)
469 stream.setVersion(QDataStream.Version.Qt_4_6)
470 while not stream.atEnd():
471 ver = stream.readUInt32()
472 if ver not in HISTORY_VERSIONS:
473 continue
474 itm = HistoryEntry()
475 itm.url = Utilities.readStringFromStream(stream)
476 stream >> itm.dateTime
477 itm.title = Utilities.readStringFromStream(stream)
478 if ver == HISTORY_VERSION_60:
479 itm.visitCount = stream.readUInt32()
480
481 if not itm.dateTime.isValid():
482 continue
483
484 if itm == lastInsertedItem:
485 if not lastInsertedItem.title and len(history) > 0:
486 history[0].title = itm.title
487 continue
488
489 if ver == HISTORY_VERSION_42:
490 firstEntry = self.__findFirstHistoryEntry(itm.url)
491 if firstEntry.isValid():
492 visitCount = firstEntry.visitCount + 1
493 self.__updateVisitCount(itm.url, visitCount)
494 else:
495 visitCount = 1
496 itm.visitCount = visitCount
497
498 if not needToSort and history and lastInsertedItem < itm:
499 needToSort = True
500
501 history.insert(0, itm)
502 lastInsertedItem = itm
503 historyFile.close()
504
505 if needToSort:
506 history.sort()
507
508 self.setHistory(history, True)
509
510 # if the history had to be sorted, rewrite the history sorted
511 if needToSort:
512 self.__lastSavedUrl = ""
513 self.__saveTimer.changeOccurred()
514
515 def save(self):
516 """
517 Public slot to save the history entries to disk.
518 """
519 historyFile = QFile(self.getFileName())
520 if not historyFile.exists():
521 self.__lastSavedUrl = ""
522
523 saveAll = self.__lastSavedUrl == ""
524 first = len(self.__history) - 1
525 if not saveAll:
526 # find the first one to save
527 for index in range(len(self.__history)):
528 if self.__history[index].url == self.__lastSavedUrl:
529 first = index - 1
530 break
531 if first == len(self.__history) - 1:
532 saveAll = True
533
534 if saveAll:
535 # use a temporary file when saving everything
536 f = QTemporaryFile()
537 f.setAutoRemove(False)
538 opened = f.open()
539 else:
540 f = historyFile
541 opened = f.open(QIODevice.OpenModeFlag.Append)
542
543 if not opened:
544 EricMessageBox.warning(
545 None,
546 self.tr("Saving History"),
547 self.tr(
548 """<p>Unable to open history file <b>{0}</b>.<br/>"""
549 """Reason: {1}</p>""")
550 .format(f.fileName(), f.errorString()))
551 return
552
553 for index in range(first, -1, -1):
554 data = QByteArray()
555 stream = QDataStream(data, QIODevice.OpenModeFlag.WriteOnly)
556 stream.setVersion(QDataStream.Version.Qt_4_6)
557 itm = self.__history[index]
558 stream.writeUInt32(HISTORY_VERSION_60)
559 stream.writeString(itm.url.encode("utf-8"))
560 stream << itm.dateTime
561 stream.writeString(itm.title.encode('utf-8'))
562 stream.writeUInt32(itm.visitCount)
563 f.write(data)
564
565 f.close()
566 if saveAll:
567 if historyFile.exists() and not historyFile.remove():
568 EricMessageBox.warning(
569 None,
570 self.tr("Saving History"),
571 self.tr(
572 """<p>Error removing old history file <b>{0}</b>."""
573 """<br/>Reason: {1}</p>""")
574 .format(historyFile.fileName(),
575 historyFile.errorString()))
576 if not f.copy(historyFile.fileName()):
577 EricMessageBox.warning(
578 None,
579 self.tr("Saving History"),
580 self.tr(
581 """<p>Error moving new history file over old one """
582 """(<b>{0}</b>).<br/>Reason: {1}</p>""")
583 .format(historyFile.fileName(), f.errorString()))
584 f.remove() # get rid of the temporary file
585 self.historySaved.emit()
586 try:
587 self.__lastSavedUrl = self.__history[0].url
588 except IndexError:
589 self.__lastSavedUrl = ""
590
591 def __refreshFrequencies(self):
592 """
593 Private slot to recalculate the refresh frequencies.
594 """
595 self.__historyFilterModel.recalculateFrequencies()
596 self.__startFrequencyTimer()
597
598 def __startFrequencyTimer(self):
599 """
600 Private method to start the timer to recalculate the frequencies.
601 """
602 tomorrow = QDateTime(QDate.currentDate().addDays(1), QTime(3, 0))
603 self.__frequencyTimer.start(
604 QDateTime.currentDateTime().secsTo(tomorrow) * 1000)
605
606 def siteVisitsCount(self, scheme, host):
607 """
608 Public method to get the visit count for a web site using the given
609 scheme.
610
611 @param scheme scheme to look for
612 @type str
613 @param host host to look for
614 @type str
615 @return number of visits to this site
616 @rtype int
617 """
618 count = 0
619 url = "{0}://{1}".format(scheme.lower(), host.lower())
620
621 seenUrls = []
622
623 for index in range(len(self.__history)):
624 historyUrl = self.__history[index].url
625 if historyUrl.startswith(url) and historyUrl not in seenUrls:
626 count += self.__history[index].visitCount
627 seenUrls.append(historyUrl)
628
629 return count

eric ide

mercurial