Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/config.py
8852 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2015-2026 Mike Fährmann
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 2 as
7
# published by the Free Software Foundation.
8
9
"""Global configuration module"""
10
11
import sys
12
import os.path
13
import logging
14
from . import util
15
16
log = logging.getLogger("config")
17
18
19
# --------------------------------------------------------------------
20
# internals
21
22
_config = {}
23
_files = []
24
_type = "json"
25
_load = util.json_loads
26
_default_configs = ()
27
28
29
# --------------------------------------------------------------------
30
# public interface
31
32
33
def default(type=None):
34
global _type
35
global _load
36
global _default_configs
37
38
if not type or (type := type.lower()) == "json":
39
_type = type = "json"
40
_load = util.json_loads
41
elif type == "yaml":
42
_type = "yaml"
43
from yaml import safe_load as _load
44
elif type == "toml":
45
_type = "toml"
46
try:
47
from tomllib import loads as _load
48
except ImportError:
49
from toml import loads as _load
50
else:
51
raise ValueError(f"Unsupported config file type '{type}'")
52
53
if util.WINDOWS:
54
_default_configs = [
55
r"%APPDATA%\gallery-dl\config." + type,
56
r"%USERPROFILE%\gallery-dl\config." + type,
57
r"%USERPROFILE%\gallery-dl.conf",
58
]
59
else:
60
_default_configs = [
61
"/etc/gallery-dl.conf",
62
"${XDG_CONFIG_HOME}/gallery-dl/config." + type
63
if os.environ.get("XDG_CONFIG_HOME") else
64
"${HOME}/.config/gallery-dl/config." + type,
65
"${HOME}/.gallery-dl.conf",
66
]
67
68
if util.EXECUTABLE:
69
# look for config file in PyInstaller executable directory (#682)
70
_default_configs.append(os.path.join(
71
os.path.dirname(sys.executable),
72
"gallery-dl.conf",
73
))
74
75
76
def initialize():
77
paths = list(map(util.expand_path, _default_configs))
78
79
for path in paths:
80
if os.access(path, os.R_OK | os.W_OK):
81
log.error("There is already a configuration file at '%s'", path)
82
return 1
83
84
for path in paths:
85
try:
86
os.makedirs(os.path.dirname(path), exist_ok=True)
87
with open(path, "x", encoding="utf-8") as fp:
88
fp.write("""\
89
{
90
"extractor": {
91
92
},
93
"downloader": {
94
95
},
96
"output": {
97
98
},
99
"postprocessor": {
100
101
}
102
}
103
""")
104
break
105
except OSError as exc:
106
log.debug("%s: %s", exc.__class__.__name__, exc)
107
else:
108
log.error("Unable to create a new configuration file "
109
"at any of the default paths")
110
return 1
111
112
log.info("Created a basic configuration file at '%s'", path)
113
return 0
114
115
116
def open_extern():
117
for path in _default_configs:
118
path = util.expand_path(path)
119
if os.access(path, os.R_OK | os.W_OK):
120
break
121
else:
122
log.warning("Unable to find any writable configuration file")
123
return 1
124
125
if util.WINDOWS:
126
openers = ("explorer", "notepad")
127
else:
128
openers = ("xdg-open", "open")
129
if editor := os.environ.get("EDITOR"):
130
openers = (editor,) + openers
131
132
import shutil
133
for opener in openers:
134
if opener := shutil.which(opener):
135
break
136
else:
137
log.warning("Unable to find a program to open '%s' with", path)
138
return 1
139
140
log.info("Running '%s %s'", opener, path)
141
retcode = util.Popen((opener, path)).wait()
142
143
if not retcode:
144
try:
145
with open(path, encoding="utf-8") as fp:
146
_load(fp.read())
147
except Exception as exc:
148
log.warning("%s when parsing '%s': %s",
149
exc.__class__.__name__, path, exc)
150
return 2
151
152
return retcode
153
154
155
def status():
156
from .output import stdout_write
157
158
paths = []
159
for path in _default_configs:
160
path = util.expand_path(path)
161
162
try:
163
with open(path, encoding="utf-8") as fp:
164
_load(fp.read())
165
except FileNotFoundError:
166
status = ""
167
except OSError as exc:
168
log.debug("%s: %s", exc.__class__.__name__, exc)
169
status = "Inaccessible"
170
except ValueError as exc:
171
log.debug("%s: %s", exc.__class__.__name__, exc)
172
status = "Invalid " + _type.upper()
173
except Exception as exc:
174
log.debug("%s: %s", exc.__class__.__name__, exc)
175
status = "Unknown"
176
else:
177
status = "OK"
178
179
paths.append((path, status))
180
181
fmt = f"{{:<{max(len(p[0]) for p in paths)}}} : {{}}\n".format
182
for path, status in paths:
183
stdout_write(fmt(path, status))
184
185
186
def remap_categories():
187
opts = _config.get("extractor")
188
if not opts:
189
return
190
191
cmap = opts.get("config-map")
192
if cmap is None:
193
cmap = (
194
("coomerparty" , "coomer"),
195
("kemonoparty" , "kemono"),
196
("giantessbooru", "sizebooru"),
197
("koharu" , "schalenetwork"),
198
("naver" , "naver-blog"),
199
("chzzk" , "naver-chzzk"),
200
("naverwebtoon", "naver-webtoon"),
201
("pixiv" , "pixiv-novel"),
202
("saint" , "turbo"),
203
)
204
elif not cmap:
205
return
206
elif isinstance(cmap, dict):
207
cmap = cmap.items()
208
209
for old, new in cmap:
210
if old in opts and new not in opts:
211
opts[new] = opts[old]
212
213
214
def load(files=None, strict=False, loads=_load, conf=_config):
215
"""Load JSON configuration files"""
216
for pathfmt in files or _default_configs:
217
path = util.expand_path(pathfmt)
218
try:
219
with open(path, encoding="utf-8") as fp:
220
config = loads(fp.read())
221
except OSError as exc:
222
if strict:
223
log.error(exc)
224
raise SystemExit(1)
225
except Exception as exc:
226
log.error("%s when loading '%s': %s",
227
exc.__class__.__name__, path, exc)
228
if strict:
229
raise SystemExit(2)
230
else:
231
if not conf:
232
conf.update(config)
233
else:
234
util.combine_dict(conf, config)
235
_files.append(pathfmt)
236
237
if "subconfigs" in config:
238
if subconfigs := config["subconfigs"]:
239
if isinstance(subconfigs, str):
240
subconfigs = (subconfigs,)
241
load(subconfigs, strict, loads, conf)
242
243
244
def clear():
245
"""Reset configuration to an empty state"""
246
_config.clear()
247
248
249
def get(path, key, default=None, conf=_config):
250
"""Get the value of property 'key' or a default value"""
251
try:
252
for p in path:
253
conf = conf[p]
254
return conf[key]
255
except Exception:
256
return default
257
258
259
def interpolate(path, key, default=None, conf=_config):
260
"""Interpolate the value of 'key'"""
261
if key in conf:
262
return conf[key]
263
try:
264
for p in path:
265
conf = conf[p]
266
if key in conf:
267
default = conf[key]
268
except Exception:
269
pass
270
return default
271
272
273
def interpolate_common(common, paths, key, default=None, conf=_config):
274
"""Interpolate the value of 'key'
275
using multiple 'paths' along a 'common' ancestor
276
"""
277
if key in conf:
278
return conf[key]
279
280
# follow the common path
281
try:
282
for p in common:
283
conf = conf[p]
284
if key in conf:
285
default = conf[key]
286
except Exception:
287
return default
288
289
# try all paths until a value is found
290
value = util.SENTINEL
291
for path in paths:
292
c = conf
293
try:
294
for p in path:
295
c = c[p]
296
if key in c:
297
value = c[key]
298
except Exception:
299
pass
300
if value is not util.SENTINEL:
301
return value
302
return default
303
304
305
def accumulate(path, key, conf=_config):
306
"""Accumulate the values of 'key' along 'path'"""
307
result = []
308
try:
309
if key in conf:
310
if value := conf[key]:
311
if isinstance(value, list):
312
result.extend(value)
313
else:
314
result.append(value)
315
for p in path:
316
conf = conf[p]
317
if key in conf:
318
if value := conf[key]:
319
if isinstance(value, list):
320
result[:0] = value
321
else:
322
result.insert(0, value)
323
except Exception:
324
pass
325
return result
326
327
328
def set(path, key, value, conf=_config):
329
"""Set the value of property 'key' for this session"""
330
for p in path:
331
try:
332
conf = conf[p]
333
except KeyError:
334
conf[p] = conf = {}
335
conf[key] = value
336
337
338
def setdefault(path, key, value, conf=_config):
339
"""Set the value of property 'key' if it doesn't exist"""
340
for p in path:
341
try:
342
conf = conf[p]
343
except KeyError:
344
conf[p] = conf = {}
345
return conf.setdefault(key, value)
346
347
348
def unset(path, key, conf=_config):
349
"""Unset the value of property 'key'"""
350
try:
351
for p in path:
352
conf = conf[p]
353
del conf[key]
354
except Exception:
355
pass
356
357
358
class apply():
359
"""Context Manager: apply a collection of key-value pairs"""
360
361
def __init__(self, kvlist):
362
self.original = []
363
self.kvlist = kvlist
364
365
def __enter__(self):
366
for path, key, value in self.kvlist:
367
self.original.append((path, key, get(path, key, util.SENTINEL)))
368
set(path, key, value)
369
370
def __exit__(self, exc_type, exc_value, traceback):
371
self.original.reverse()
372
for path, key, value in self.original:
373
if value is util.SENTINEL:
374
unset(path, key)
375
else:
376
set(path, key, value)
377
378
379
default(os.environ.get("GDL_CONFIG_TYPE"))
380
381