Skip to content

Commit 8995db4

Browse files
bossanova808bossanova808
andauthored
[script.cabertoss] 1.0.2 (#2772)
Co-authored-by: bossanova808 <[email protected]>
1 parent 048c0f1 commit 8995db4

File tree

7 files changed

+140
-77
lines changed

7 files changed

+140
-77
lines changed

script.cabertoss/addon.xml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="script.cabertoss" name="Caber Toss" version="1.0.1" provider-name="bossanova808">
2+
<addon id="script.cabertoss" name="Caber Toss" version="1.0.2" provider-name="bossanova808">
33
<requires>
44
<import addon="xbmc.python" version="3.0.0"/>
55
<import addon="script.module.bossanova808" version="1.0.0"/>
@@ -22,13 +22,10 @@ Even better, bind this to a remote button (use `Runscript(script.cabertoss)`) -
2222
<source>https://github.com/bossanova808/script.cabertoss</source>
2323
<forum>https://forum.kodi.tv/showthread.php?tid=379304</forum>
2424
<email>[email protected]</email>
25-
<news>v1.0.1
26-
- Improve compatibility with network shares
27-
- Improve compatibility with Windows and *ELEC
28-
- Add hostname to logs folder, helps if using with multiple systems
29-
- Don't copy crashlogs older than three days
30-
- Language improvements
31-
- Add 'Working...' notification to make more responsive (copying larger log files can take a moment)
25+
<news>v1.0.2
26+
- Use updated Logging code
27+
- Make crashlog days configurable (default still 3)
28+
- A bunch of fixes and improvements following CodeRabbit review
3229
</news>
3330
<assets>
3431
<icon>resources/icon.png</icon>

script.cabertoss/changelog.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
v1.0.2
2+
- Use updated Logging code
3+
- Make crashlog days configurable (default still 3)
4+
- A bunch of fixes and improvements following CodeRabbit review
5+
16
v1.0.1
27
- Improve compatibility with network shares
38
- Improve compatibility with Windows and *ELEC

script.cabertoss/resources/language/resource.language.en_gb/strings.po

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,36 @@ msgstr ""
2727

2828
msgctxt "#32024"
2929
msgid "Android crashlogs are not supported, sorry"
30-
msgstr "
30+
msgstr ""
3131

3232
msgctxt "#32025"
3333
msgid "No log files found to copy ?!"
34-
msgstr "
34+
msgstr ""
3535

3636
msgctxt "#32026"
3737
msgid "Error copying logs"
38-
msgstr "
38+
msgstr ""
3939

4040
msgctxt "#32027"
4141
msgid "No destination path set in the addon settings!"
42-
msgstr "
42+
msgstr ""
4343

4444
msgctxt "#32028"
4545
msgid "Log files copied"
46-
msgstr "
46+
msgstr ""
4747

4848
msgctxt "#32029"
4949
msgid "Something went wrong, (ironically) check your logs!"
50-
msgstr "
50+
msgstr ""
5151

5252
msgctxt "#32030"
5353
msgid "Working..."
54-
msgstr "
54+
msgstr ""
55+
56+
msgctxt "#32031"
57+
msgid "Error making directory to copy log files to!"
58+
msgstr ""
59+
60+
msgctxt "#32032"
61+
msgid "Max age of crashlogs to copy (days)"
62+
msgstr ""
Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
# -*- coding: utf-8 -*-
22
import os
3-
from datetime import datetime
4-
from time import sleep
53
from datetime import datetime, timedelta
64
import socket
5+
from typing import List, Tuple
76

87
import xbmc
98
import xbmcvfs
10-
from bossanova808.constants import *
11-
from bossanova808.utilities import *
9+
from bossanova808.constants import LOG_PATH
10+
from bossanova808.constants import LANGUAGE
1211
from bossanova808.logger import Logger
1312
from bossanova808.notify import Notify
1413
from resources.lib.store import Store
15-
from resources.lib.clean import *
14+
from resources.lib.clean import clean_log
1615

1716

18-
def gather_log_files():
17+
def _vfs_join(base: str, name: str) -> str:
18+
if base.startswith(('special://', 'smb://', 'nfs://', 'ftp://', 'http://', 'https://')):
19+
return base.rstrip('/') + '/' + name
20+
return os.path.join(base, name)
21+
22+
23+
def gather_log_files() -> List[Tuple[str, str]]:
1924
"""
2025
Gather a list of the standard Kodi log files (Kodi.log, Kodi.old.log) and the latest crash log, if there is one.
2126
22-
@return: list of log files in form [type, path], where type is log, oldlog, or crashlog
27+
@return: list of log files as (type, path) tuples, where type is 'log', 'oldlog', or 'crashlog'
2328
"""
2429

2530
# Basic log files
26-
log_files = [['log', os.path.join(LOG_PATH, 'kodi.log')]]
27-
if os.path.exists(os.path.join(LOG_PATH, 'kodi.old.log')):
28-
log_files.append(['oldlog', os.path.join(LOG_PATH, 'kodi.old.log')])
31+
log_files = [('log', os.path.join(LOG_PATH, 'kodi.log'))]
32+
if xbmcvfs.exists(os.path.join(LOG_PATH, 'kodi.old.log')):
33+
log_files.append(('oldlog', os.path.join(LOG_PATH, 'kodi.old.log')))
2934

3035
# Can we find a crashlog?
3136
# @TODO - add Android support if possible..?
@@ -50,7 +55,7 @@ def gather_log_files():
5055
filematch = 'kodi_'
5156
elif xbmc.getCondVisibility('system.platform.android'):
5257
Logger.info("System is Android")
53-
Logger.info(LANGUAGE(32023))
58+
Logger.info(LANGUAGE(32024))
5459

5560
# If *ELEC, we can be more specific
5661
if xbmc.getCondVisibility('System.HasAddon(service.coreelec.settings)') or xbmc.getCondVisibility('System.HasAddon(service.libreelec.settings)'):
@@ -59,18 +64,16 @@ def gather_log_files():
5964
filematch = 'kodi_crashlog_'
6065

6166
if crashlog_path and os.path.isdir(crashlog_path):
62-
lastcrash = None
6367
dirs, possible_crashlog_files = xbmcvfs.listdir(crashlog_path)
6468
for item in possible_crashlog_files:
6569
item_with_path = os.path.join(crashlog_path, item)
6670
if filematch in item and os.path.isfile(item_with_path):
67-
if filematch in item:
68-
# Don't bother with older crashlogs
69-
three_days_ago = datetime.now() - timedelta(days=3)
70-
if three_days_ago < datetime.fromtimestamp(os.path.getmtime(item_with_path)):
71-
items.append(os.path.join(crashlog_path, item))
71+
# Don't bother with older crashlogs
72+
x_days_ago = datetime.now() - timedelta(days=Store.crashlog_max_days)
73+
if x_days_ago < datetime.fromtimestamp(os.path.getmtime(item_with_path)):
74+
items.append(os.path.join(crashlog_path, item))
7275

73-
items.sort(key=lambda f: os.path.getmtime(f))
76+
items.sort(key=lambda f:os.path.getmtime(f))
7477
# Windows crashlogs are a dmp and stacktrace combo...
7578
if xbmc.getCondVisibility('system.platform.windows'):
7679
lastcrash = items[-2:]
@@ -80,43 +83,59 @@ def gather_log_files():
8083
if lastcrash:
8184
# Logger.info(f"lastcrash {lastcrash}")
8285
for crashfile in lastcrash:
83-
log_files.append(['crashlog', crashfile])
86+
log_files.append(('crashlog', crashfile))
8487

85-
Logger.info("Found these log files to copy:")
86-
Logger.info(log_files)
88+
Logger.info("Found these log files to copy (type, basename):")
89+
Logger.info([[t, os.path.basename(p)] for t, p in log_files])
8790

8891
return log_files
8992

9093

91-
def copy_log_files(log_files: []):
94+
def copy_log_files(log_files: List[Tuple[str, str]]) -> bool:
9295
"""
93-
Actually copy the log files to the path in the addon settings
96+
Copy the provided Kodi log files into a timestamped destination folder under the configured addon destination.
97+
98+
Detailed behavior:
99+
- Expects log_files as List[Tuple[str, str]] where `type` is e.g. 'log', 'oldlog', or 'crashlog' and `path` is the source filesystem path.
100+
- Creates a destination directory at Store.destination_path named "<hostname>_Kodi_Logs_<YYYY-MM-DD_HH-MM-SS>".
101+
- For entries with type 'log' or 'oldlog', reads the source, sanitises the content with clean_log() (because the log content may contain URLs with embedded user/password details), and writes the sanitised content to a file with the same basename in the destination folder.
102+
- For other types (e.g., crash logs), copies the source file to the destination folder unchanged.
94103
95-
@param log_files: [] list of log files to copy
96-
@return: None
104+
Parameters:
105+
log_files (List[Tuple[str, str]]): list of log descriptors [type, path] to copy.
106+
107+
Returns:
108+
bool: True if files were successfully copied, False otherwise.
97109
"""
98110
if not log_files:
99111
Logger.error(LANGUAGE(32025))
100112
Notify.error(LANGUAGE(32025))
101-
return
113+
return False
102114

103115
now_folder_name = f"{socket.gethostname()}_Kodi_Logs_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}"
104-
now_destination_path = os.path.join(Store.destination_path, now_folder_name)
116+
now_destination_path = _vfs_join(Store.destination_path, now_folder_name)
105117

106118
try:
107119
Logger.info(f'Making destination folder: {now_destination_path}')
108-
xbmcvfs.mkdir(now_destination_path)
120+
if not xbmcvfs.mkdirs(now_destination_path):
121+
Logger.error(f'Failed to create destination folder: {now_destination_path}')
122+
Notify.error(LANGUAGE(32031))
123+
return False
109124
for file in log_files:
110125
if file[0] in ['log', 'oldlog']:
111126
Logger.info(f'Copying sanitised {file[0]} {file[1]}')
112-
with open(xbmcvfs.translatePath(file[1]), 'r', encoding='utf-8') as current:
127+
with open(xbmcvfs.translatePath(file[1]), 'r', encoding='utf-8', errors='replace') as current:
113128
content = current.read()
114129
sanitised = clean_log(content)
115-
with xbmcvfs.File(os.path.join(xbmcvfs.translatePath(now_destination_path),os.path.basename(file[1])), 'w') as output:
116-
output.write(sanitised)
130+
dest_path = _vfs_join(now_destination_path, os.path.basename(file[1]))
131+
f = xbmcvfs.File(dest_path, 'w')
132+
try:
133+
f.write(sanitised.encode('utf-8'))
134+
finally:
135+
f.close()
117136
else:
118137
Logger.info(f'Copying {file[0]} {file[1]}')
119-
if not xbmcvfs.copy(file[1], os.path.join(now_destination_path, os.path.basename(file[1]))):
138+
if not xbmcvfs.copy(file[1], _vfs_join(now_destination_path, os.path.basename(file[1]))):
120139
return False
121140
return True
122141

@@ -128,18 +147,31 @@ def copy_log_files(log_files: []):
128147

129148
# This is 'main'...
130149
def run():
131-
footprints()
132-
Store.load_config_from_settings()
133-
134-
if not Store.destination_path:
135-
Notify.error(LANGUAGE(32027))
136-
else:
137-
Notify.info(LANGUAGE(32030))
138-
log_file_list = gather_log_files()
139-
result = copy_log_files(log_file_list)
140-
if result:
141-
Notify.info(LANGUAGE(32028) + f": {len(log_file_list)}")
150+
"""
151+
Run the log collection and copying flow: initialize this addon's logging, load configuration, gather Kodi log files, copy them to the configured destination, notify the user, and stop this addon's logging.
152+
153+
This function performs the module's main orchestration. It:
154+
- Starts the logger for this addon's internal logging (not Kodi's general logging system) and loads addon configuration from settings.
155+
- If no destination path is configured, shows an error notification and skips copying.
156+
- Otherwise, notifies the user, gathers available log files, attempts to copy them to the configured destination, and notifies success (including number of files copied) or failure.
157+
- Stops this addon's internal logging before returning.
158+
159+
Side effects: starts/stops this addon's internal logging, reads configuration, performs filesystem operations (reading, sanitizing, and copying log files), and shows user notifications. Returns None.
160+
"""
161+
Logger.start()
162+
try:
163+
Store.load_config_from_settings()
164+
165+
if not Store.destination_path:
166+
Notify.error(LANGUAGE(32027))
142167
else:
143-
Notify.info(LANGUAGE(32029))
168+
Notify.info(LANGUAGE(32030))
169+
log_file_list = gather_log_files()
170+
result = copy_log_files(log_file_list)
171+
if result:
172+
Notify.info(LANGUAGE(32028) + f": {len(log_file_list)}")
173+
else:
174+
Notify.error(LANGUAGE(32029))
144175
# and, we're done...
145-
footprints(startup=False)
176+
finally:
177+
Logger.stop()
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import re
22

33

4-
def clean_log(content):
4+
def clean_log(content: str) -> str:
55
"""
66
Remove username/password details from log file content
77
88
@param content:
99
@return:
1010
"""
11-
replaces = (('//.+?:.+?@', '//USER:PASSWORD@'), ('<user>.+?</user>', '<user>USER</user>'), ('<pass>.+?</pass>', '<pass>PASSWORD</pass>'),)
11+
replaces = (
12+
# Replace only the credentials part between '//' and '@'
13+
(r'(?<=//)([^/@:\s]+):([^/@\s]+)@', r'USER:PASSWORD@'),
14+
# Also scrub username only (no password)
15+
(r'(?<=//)([^/@:\s]+)@', r'USER@'),
16+
# Replace XML username/password; keep it local to the tag
17+
(r'(?is)<user>[^<]*</user>', r'<user>USER</user>'),
18+
(r'(?is)<pass>[^<]*</pass>', r'<pass>PASSWORD</pass>'),
19+
)
1220

1321
for pattern, repl in replaces:
14-
sanitised = re.sub(pattern, repl, content)
15-
return sanitised
22+
content = re.sub(pattern, repl, content)
23+
return content
Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from bossanova808.constants import *
1+
from bossanova808.constants import ADDON
22
from bossanova808.logger import Logger
3-
from resources.lib.clean import *
3+
from resources.lib.clean import clean_log
44

55

66
class Store:
@@ -13,7 +13,8 @@ class Store:
1313

1414
# Static class variables, referred to elsewhere by Store.whatever
1515
# https://docs.python.org/3/faq/programming.html#how-do-i-create-static-class-data-and-static-class-methods
16-
destination_path = None
16+
destination_path: str = ''
17+
crashlog_max_days: int = 3
1718

1819
def __init__(self):
1920
"""
@@ -24,15 +25,15 @@ def __init__(self):
2425
@staticmethod
2526
def load_config_from_settings():
2627
"""
27-
Load in the addon settings, at start or reload them if they have been changed
28-
Log each setting as it is loaded
28+
Load the addon's configuration from persistent settings.
29+
30+
Reads the 'log_path' setting and assigns it to Store.destination_path, then logs the resolved path (sanitized with clean_log because these paths may be URLs with embedded user/password details). This is called at startup and when settings are reloaded; it has no return value.
2931
"""
3032
Logger.info("Loading configuration from settings")
31-
Store.destination_path = ADDON.getSetting('log_path')
32-
33-
Logger.info(f'Logs will be tossed to: {clean_log(Store.destination_path)}')
34-
35-
36-
37-
38-
33+
Store.destination_path = ADDON.getSetting('log_path') or ''
34+
if Store.destination_path:
35+
Logger.info(f'Logs will be tossed to: {clean_log(Store.destination_path)}')
36+
else:
37+
Logger.warning('No path set to toss logs to.')
38+
Store.crashlog_max_days = int(ADDON.getSetting('crashlog_max_days')) or 3
39+
Logger.info(f'Crashlog max days: {Store.crashlog_max_days}')

script.cabertoss/resources/settings.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717
<heading>32001</heading>
1818
</control>
1919
</setting>
20+
<setting id="crashlog_max_days" type="integer" label="32032" help="">
21+
<level>0</level>
22+
<default>3</default>
23+
<constraints>
24+
<minimum>0</minimum>
25+
<maximum>365</maximum>
26+
<step>1</step>
27+
</constraints>
28+
<control type="edit" format="integer">
29+
<heading>32032</heading>
30+
</control>
31+
</setting>
2032
</group>
2133
</category>
2234
</section>

0 commit comments

Comments
 (0)