153 lines
4.7 KiB
Python
153 lines
4.7 KiB
Python
"""
|
||
novelWriter – Project Session Log Class
|
||
=======================================
|
||
|
||
File History:
|
||
Created: 2023-06-11 [2.1b1] NWSessionLog
|
||
|
||
This file is a part of novelWriter
|
||
Copyright (C) 2023 Veronica Berglyd Olsen and novelWriter contributors
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful, but
|
||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
""" # noqa
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
|
||
from pathlib import Path
|
||
from time import time
|
||
from typing import TYPE_CHECKING
|
||
|
||
from novelwriter.common import formatTimeStamp
|
||
from novelwriter.constants import nwFiles
|
||
from novelwriter.error import logException
|
||
|
||
if TYPE_CHECKING:
|
||
from collections.abc import Iterable
|
||
|
||
from novelwriter.core.project import NWProject
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class NWSessionLog:
|
||
"""Core: Session JSON Lines Log File.
|
||
|
||
The class that wraps the session log file, which is in JSON Lines
|
||
format. That is, one JSON object per line.
|
||
"""
|
||
|
||
def __init__(self, project: NWProject) -> None:
|
||
self._project = project
|
||
self._start = 0.0
|
||
|
||
##
|
||
# Properties
|
||
##
|
||
|
||
@property
|
||
def start(self) -> float:
|
||
"""The session start time."""
|
||
return self._start
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def startSession(self) -> None:
|
||
"""Start the writing session."""
|
||
self._start = time()
|
||
|
||
def appendSession(self, idleTime: float) -> bool:
|
||
"""Append session statistics to the sessions log file."""
|
||
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
|
||
if not isinstance(sessFile, Path):
|
||
return False
|
||
|
||
now = time()
|
||
iWNovel, iWNotes, iCNovel, iCNotes = self._project.data.initCounts
|
||
cWNovel, cWNotes, cCNovel, cCNotes = self._project.data.currCounts
|
||
iWTotal = iWNovel + iWNotes
|
||
iCTotal = iCNovel + iCNotes
|
||
wDiff = cWNovel + cWNotes - iWTotal
|
||
cDiff = cCNovel + cCNotes - iCTotal
|
||
sTime = now - self._start
|
||
|
||
logger.info(
|
||
"The session lasted %d sec and added %d words abd %d characters",
|
||
int(sTime), wDiff, cDiff
|
||
)
|
||
if sTime < 300 and (wDiff == 0 or cDiff == 0):
|
||
logger.info("Session too short, skipping log entry")
|
||
return False
|
||
|
||
try:
|
||
if not sessFile.exists():
|
||
with open(sessFile, mode="w", encoding="utf-8") as fObj:
|
||
fObj.write(self.createInitial(iWTotal))
|
||
|
||
with open(sessFile, mode="a+", encoding="utf-8") as fObj:
|
||
fObj.write(self.createRecord(
|
||
start=formatTimeStamp(self._start),
|
||
end=formatTimeStamp(now),
|
||
novel=cWNovel,
|
||
notes=cWNotes,
|
||
idle=round(idleTime),
|
||
cnovel=cCNovel,
|
||
cnotes=cCNotes,
|
||
))
|
||
|
||
except Exception:
|
||
logger.error("Failed to write to session stats file")
|
||
logException()
|
||
return False
|
||
|
||
return True
|
||
|
||
def iterRecords(self) -> Iterable[dict]:
|
||
"""Iterate through all records in the log."""
|
||
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
|
||
if isinstance(sessFile, Path) and sessFile.is_file():
|
||
try:
|
||
with open(sessFile, mode="r", encoding="utf-8") as fObj:
|
||
for line in fObj:
|
||
yield json.loads(line)
|
||
except Exception:
|
||
logger.error("Failed to process session stats file")
|
||
logException()
|
||
return
|
||
|
||
def createInitial(self, total: int) -> str:
|
||
"""Low level function to create the initial log file record."""
|
||
data = json.dumps({"type": "initial", "offset": total})
|
||
return f"{data}\n"
|
||
|
||
def createRecord(
|
||
self, start: str, end: str, novel: int, notes: int, idle: int,
|
||
cnovel: int = 0, cnotes: int = 0,
|
||
) -> str:
|
||
"""Low level function to create a log record."""
|
||
data = json.dumps({
|
||
"type": "record",
|
||
"start": start,
|
||
"end": end,
|
||
"novel": novel,
|
||
"notes": notes,
|
||
"cnovel": cnovel,
|
||
"cnotes": cnotes,
|
||
"idle": idle,
|
||
})
|
||
return f"{data}\n"
|