srcalchemy 0.2.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.
srcalchemy/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ #################################################################################
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2025 Duncan Fraser
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #################################################################################
24
+ #
25
+ # File : __init__.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+
33
+ from .srcalchemy import main
34
+
35
+ try:
36
+ # If the package is installed/built, this is the fastest, cleanest approach
37
+ from importlib.metadata import PackageNotFoundError, version
38
+
39
+ __version__ = version("srcalchemy")
40
+ except PackageNotFoundError:
41
+ # Fallback for local, non-installed development folders
42
+ try:
43
+ from ._version import __version__
44
+ except ImportError:
45
+ __version__ = "0.0.0-unknown"
46
+
47
+ if __name__ == "__main__":
48
+ main()
srcalchemy/__main__.py ADDED
@@ -0,0 +1,36 @@
1
+ #################################################################################
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2025 Duncan Fraser
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #################################################################################
24
+ #
25
+ # File : __main__.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+
33
+ from srcalchemy import main
34
+
35
+ if __name__ == "__main__":
36
+ main()
srcalchemy/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,63 @@
1
+ #################################################################################
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2025 Duncan Fraser
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #################################################################################
24
+ #
25
+ # File : __init__.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+
33
+ from .collector import (
34
+ EpisodeInfo,
35
+ MovieInfo,
36
+ TVInfo,
37
+ deserializeMovieInfo,
38
+ deserializeTVInfo,
39
+ fileInfo,
40
+ getMovieInfo,
41
+ getTvInfo,
42
+ movieInfoStatistics,
43
+ parseFileInfo,
44
+ serializeMovieInfo,
45
+ serializeTVInfo,
46
+ tvInfoStatistics,
47
+ )
48
+
49
+ __all__ = [
50
+ "EpisodeInfo",
51
+ "MovieInfo",
52
+ "TVInfo",
53
+ "deserializeMovieInfo",
54
+ "deserializeTVInfo",
55
+ "fileInfo",
56
+ "getMovieInfo",
57
+ "getTvInfo",
58
+ "movieInfoStatistics",
59
+ "parseFileInfo",
60
+ "serializeMovieInfo",
61
+ "serializeTVInfo",
62
+ "tvInfoStatistics",
63
+ ]
@@ -0,0 +1,503 @@
1
+ #################################################################################
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2025 Duncan Fraser
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #################################################################################
24
+ #
25
+ # File : collector.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+
33
+ import os
34
+ import pickle
35
+ from dataclasses import dataclass, field
36
+ from typing import BinaryIO, Final
37
+
38
+ import requests
39
+
40
+ from srcalchemy.config import ConfigSpec, driveMappingConfig
41
+
42
+ videoSizeMap: Final[list[dict]] = [
43
+ {"min": 0, "max": 360, "label": "SD"},
44
+ {"min": 361, "max": 480, "label": "480"},
45
+ {"min": 481, "max": 576, "label": "576"},
46
+ {"min": 577, "max": 720, "label": "720p"},
47
+ {"min": 721, "max": 1080, "label": "1080p"},
48
+ {"min": 1081, "max": 100000, "label": "2160p"},
49
+ ]
50
+
51
+
52
+ @dataclass
53
+ class fileInfo:
54
+ """File information"""
55
+
56
+ path: str
57
+ """Full path"""
58
+ parentPath: str
59
+ """Parent directory path"""
60
+ extension: str
61
+ """File extension"""
62
+ filename: str
63
+ """File filename"""
64
+ basePath: str
65
+ """Base Common Path"""
66
+
67
+
68
+ def parseFileInfo(path: str, base_path: str) -> fileInfo:
69
+ """Parse path strings to File Information
70
+
71
+ :param path: Full path to file
72
+ :param base_path: Base Common Path
73
+
74
+ :returns: File Information
75
+ """
76
+ extension = os.path.splitext(path)[-1:][0]
77
+ filename = os.path.splitext(os.path.basename(path))[0]
78
+ parentPath = os.path.dirname(path)
79
+ return fileInfo(
80
+ path=path,
81
+ parentPath=parentPath,
82
+ extension=extension,
83
+ filename=filename,
84
+ basePath=base_path,
85
+ )
86
+
87
+
88
+ def parseVideoFormat(video_details: list[dict]) -> str:
89
+ """Use video details from Kodi JSONRPC to get the Video Format
90
+
91
+ :params video_details: Video details extracted from kodi data
92
+
93
+ :returns: String describing item Video Format
94
+ """
95
+ match_label = "SD"
96
+
97
+ if (
98
+ len(video_details) > 0
99
+ and video_details[0]["height"]
100
+ and (video_details[0]["height"] > 0)
101
+ ):
102
+ match_label = next(
103
+ x["label"]
104
+ for x in videoSizeMap
105
+ if x["min"] <= video_details[0]["height"]
106
+ and video_details[0]["height"] <= x["max"]
107
+ )
108
+
109
+ return match_label
110
+
111
+
112
+ def replaceDriveMapping(path: str, drive_mapping: list[driveMappingConfig]) -> str:
113
+ """Replace paths using the available drive mappings
114
+
115
+ :param path: Path to file
116
+ :param drive_mapping: List of path replacements
117
+
118
+ :returns: Path where path is replaced with available drive mappings
119
+ """
120
+ for mapping in drive_mapping:
121
+ if mapping.src == "":
122
+ break
123
+
124
+ if path.startswith(mapping.src):
125
+ return path.replace(mapping.src, mapping.dest)
126
+
127
+ return path
128
+
129
+
130
+ def slugify(source_string: str, remove_string: str = "%:/,.\\[]<>*?") -> str:
131
+ """Remove undesirable characters from string
132
+
133
+ :param source_string: Source string to perform remove on
134
+ :param remove_string: String containing characters to remove from source string
135
+
136
+ :returns: Source string without characters in remove_string
137
+ """
138
+ # remove the undesirable characters
139
+ return "".join([c for c in source_string if c not in remove_string])
140
+
141
+
142
+ @dataclass
143
+ class MovieInfo:
144
+ """Movie Information describing items parsed from Kodi"""
145
+
146
+ title: str
147
+ """Movie Title"""
148
+ year: int
149
+ """Release Year"""
150
+ file: fileInfo
151
+ """Movie File Information"""
152
+ formatString: str
153
+ """Video Format, derived from video """
154
+ data: dict | None = None
155
+ """Optional raw data fetched from Kodi"""
156
+
157
+
158
+ @dataclass
159
+ class EpisodeInfo:
160
+ """Episode Information describing items parsed from Kodi"""
161
+
162
+ title: str
163
+ """Episode Title"""
164
+ season: int
165
+ """Season number"""
166
+ episode: int
167
+ """Episode number within season"""
168
+ tvShowId: int
169
+ """TV Show ID"""
170
+ file: fileInfo
171
+ """Episode File Information"""
172
+ data: dict | None = None
173
+ """Optional raw data fetched from Kodi"""
174
+
175
+
176
+ @dataclass
177
+ class TVInfo:
178
+ """TV Information describing items parsed from Kodi"""
179
+
180
+ title: str
181
+ """TV Title"""
182
+ year: int
183
+ """Release Year"""
184
+ tvShowId: int
185
+ """TV Show ID"""
186
+ seasonSet: set[int] = field(default_factory=set)
187
+ """Set containing season numbers"""
188
+ episodes: list[EpisodeInfo] = field(default_factory=list)
189
+ """List of episodes information"""
190
+ data: dict | None = None
191
+ """Optional raw data fetched from Kodi"""
192
+
193
+
194
+ def getMovieData(config: ConfigSpec, start: int = 0, end: int = -1) -> list[dict]:
195
+ """Get Movie Information from Kodi JSONRPC
196
+
197
+ :param config: Configuration object
198
+ :param start: Start ID to fetch
199
+ :param end: End ID to fetch
200
+
201
+ :returns: Dictionary List from Raw Data from JSONRPC
202
+ """
203
+ id = 1010
204
+ data = {
205
+ "jsonrpc": "2.0",
206
+ "method": "VideoLibrary.GetMovies",
207
+ "params": {
208
+ "properties": ["title", "year", "file", "streamdetails"],
209
+ "limits": {"start": start, "end": end},
210
+ },
211
+ "id": id,
212
+ }
213
+ url = "{}://{}:{}/jsonrpc".format(
214
+ "https" if config.kodi.use_https else "http",
215
+ config.kodi.host,
216
+ config.kodi.host_port,
217
+ )
218
+ req = requests.post(url=url, json=data, verify=False)
219
+
220
+ if req.status_code == 200:
221
+ return list(req.json()["result"]["movies"])
222
+ else:
223
+ return []
224
+
225
+
226
+ def getTvData(config: ConfigSpec, start: int = 0, end: int = -1) -> list[dict]:
227
+ """Get TV Information from Kodi JSONRPC
228
+
229
+ :param config: Configuration object
230
+ :param start: Start ID to fetch
231
+ :param end: End ID to fetch
232
+
233
+ :returns: Dictionary List from Raw Data from JSONRPC
234
+ """
235
+ id = 2020
236
+ data = {
237
+ "jsonrpc": "2.0",
238
+ "method": "VideoLibrary.GetTVShows",
239
+ "params": {
240
+ "properties": ["title", "year"],
241
+ "limits": {"start": start, "end": end},
242
+ },
243
+ "id": id,
244
+ }
245
+ url = "{}://{}:{}/jsonrpc".format(
246
+ "https" if config.kodi.use_https else "http",
247
+ config.kodi.host,
248
+ config.kodi.host_port,
249
+ )
250
+ req = requests.post(url=url, json=data, verify=False)
251
+
252
+ if req.status_code == 200:
253
+ return list(req.json()["result"]["tvshows"])
254
+ else:
255
+ return []
256
+
257
+
258
+ def getEpisodeData(config: ConfigSpec, start: int = 0, end: int = -1) -> list[dict]:
259
+ """Get Episode Information from Kodi JSONRPC
260
+
261
+ :param config: Configuration object
262
+ :param start: Start ID to fetch
263
+ :param end: End ID to fetch
264
+
265
+ :returns: Dictionary List from Raw Data from JSONRPC
266
+ """
267
+ id = 3030
268
+ data = {
269
+ "jsonrpc": "2.0",
270
+ "method": "VideoLibrary.GetEpisodes",
271
+ "params": {
272
+ "properties": [
273
+ "title",
274
+ "tvshowid",
275
+ "season",
276
+ "episode",
277
+ "showtitle",
278
+ "file",
279
+ ],
280
+ "limits": {"start": start, "end": end},
281
+ },
282
+ "id": id,
283
+ }
284
+ url = f"http://{config.kodi.host}:{config.kodi.host_port}/jsonrpc"
285
+ req = requests.post(url=url, json=data, verify=False)
286
+
287
+ if req.status_code == 200:
288
+ return list(req.json()["result"]["episodes"])
289
+ else:
290
+ return []
291
+
292
+
293
+ def parseMovieInfo(
294
+ config: ConfigSpec, data: dict, appendRaw: bool = False
295
+ ) -> MovieInfo:
296
+ """Parse data from Kodi JSONRPC into movie information
297
+
298
+ :param config: Configuration object
299
+ :param data: Data from Kodi JSONRPC
300
+ :param appendRaw: Store data param in output if True
301
+
302
+ :returns: Movie Information
303
+ """
304
+ file_path = replaceDriveMapping(
305
+ path=data["file"], drive_mapping=config.driveMapping
306
+ )
307
+ movie_info = MovieInfo(
308
+ title=slugify(data["title"]),
309
+ file=parseFileInfo(file_path, config.movie_config.basePath),
310
+ formatString=parseVideoFormat(data["streamdetails"]["video"]),
311
+ year=data["year"],
312
+ data=(data if appendRaw else None),
313
+ )
314
+
315
+ return movie_info
316
+
317
+
318
+ def parseTvInfo(data: dict, appendRaw: bool = False) -> TVInfo:
319
+ """Parse data from Kodi JSONRPC into TV information
320
+
321
+ :param data: Data from Kodi JSONRPC
322
+ :param appendRaw: Store data param in output if True
323
+
324
+ :returns: TV Information
325
+ """
326
+ return TVInfo(
327
+ title=slugify(data["title"]),
328
+ year=data["year"],
329
+ tvShowId=data["tvshowid"],
330
+ data=(data if appendRaw else None),
331
+ )
332
+
333
+
334
+ def parseEpisodeInfo(
335
+ config: ConfigSpec, data: dict, appendRaw: bool = False
336
+ ) -> EpisodeInfo:
337
+ """Parse data from Kodi JSONRPC into episode information
338
+
339
+ :param config: Configuration object
340
+ :param data: Data from Kodi JSONRPC
341
+ :param appendRaw: Store data param in output if True
342
+
343
+ :returns: Movie Information
344
+ """
345
+ file_path = replaceDriveMapping(
346
+ path=data["file"], drive_mapping=config.driveMapping
347
+ )
348
+ episode_info = EpisodeInfo(
349
+ title=slugify(data["title"]),
350
+ season=data["season"],
351
+ episode=data["episode"],
352
+ file=parseFileInfo(file_path, config.tv_config.basePath),
353
+ tvShowId=data["tvshowid"],
354
+ data=(data if appendRaw else None),
355
+ )
356
+
357
+ return episode_info
358
+
359
+
360
+ def getMovieInfo(config: ConfigSpec) -> list[MovieInfo]:
361
+ """Collect all Movies from Kodi and parse into Movie Information
362
+
363
+ :param config: Configuration object
364
+
365
+ :returns: List of Movie Information collected from Kodi JSONRPC
366
+ """
367
+ movie_info: list[MovieInfo] = []
368
+
369
+ movie_data = getMovieData(config=config, start=0, end=0)
370
+
371
+ for movie in movie_data:
372
+ movie_info.append(parseMovieInfo(config=config, data=movie))
373
+
374
+ return movie_info
375
+
376
+
377
+ def getTvInfo(config: ConfigSpec) -> list[TVInfo]:
378
+ """Collect all Episodes and TV Shows from Kodi and parse into TV Information
379
+
380
+ :param config: Configuration object
381
+
382
+ :returns: List of TV Information collected from Kodi JSONRPC
383
+ """
384
+ tv_shows: dict[int, TVInfo] = {}
385
+
386
+ tv_data = getTvData(config=config, start=0, end=0)
387
+
388
+ for tv in tv_data:
389
+ tv_info = parseTvInfo(tv)
390
+ tv_shows[tv_info.tvShowId] = tv_info
391
+
392
+ episode_data = getEpisodeData(config=config, start=0, end=0)
393
+
394
+ for episode in episode_data:
395
+ episode_info = parseEpisodeInfo(config=config, data=episode)
396
+
397
+ if episode_info.tvShowId in tv_shows:
398
+ tv_shows[episode_info.tvShowId].episodes.append(episode_info)
399
+ tv_shows[episode_info.tvShowId].seasonSet.add(episode_info.season)
400
+
401
+ tv_info_list: list[TVInfo] = []
402
+ for value in tv_shows.values():
403
+ tv_info_list.append(value)
404
+
405
+ return tv_info_list
406
+
407
+
408
+ def serializeMovieInfo(pickleIO: BinaryIO, movieInfo: list[MovieInfo]) -> None:
409
+ """Write Movie Information to File
410
+
411
+ :param pickleIO: Writeable Binary I/O object
412
+ :param movieInfo: List of Movie Information
413
+
414
+ .. code-block:: python
415
+
416
+ movie_info = []
417
+ with open("output", "wb") as file:
418
+ srcalchemy.collector.serializeMovieInfo(pickleIo=file, movieInfo=movie_info)
419
+
420
+ """
421
+ pickle.dump(movieInfo, file=pickleIO)
422
+
423
+
424
+ def deserializeMovieInfo(pickleIO: BinaryIO) -> list[MovieInfo]:
425
+ """Get Movie Information from File
426
+
427
+ :param pickleIO: Readable Binary I/O object
428
+
429
+ :returns: Movie Information
430
+
431
+ .. code-block:: python
432
+
433
+ with open("output", "rb") as file:
434
+ movie_info = srcalchemy.collector.deserializeMovieInfo(pickleIo=file)
435
+ """
436
+ return pickle.load(pickleIO)
437
+
438
+
439
+ def serializeTVInfo(pickleIO: BinaryIO, tvInfo: list[TVInfo]) -> None:
440
+ """Write Movie Information to File
441
+
442
+ :param pickleIO: Writable Binary I/O object
443
+ :param movieInfo: List of TV Information
444
+
445
+ .. code-block:: python
446
+
447
+ tv_info = []
448
+ with open("output", "wb") as file:
449
+ srcalchemy.collector.serializeTVInfo(pickleIo=file, movieInfo=tv_info)
450
+
451
+ """
452
+ pickle.dump(tvInfo, file=pickleIO)
453
+
454
+
455
+ def deserializeTVInfo(pickleIO: BinaryIO) -> list[TVInfo]:
456
+ """Get Movie Information from File
457
+
458
+ :param pickleIO: Readable Binary I/O object
459
+
460
+ :returns: List of TV Information
461
+
462
+ .. code-block:: python
463
+
464
+ with open("output", "rb") as file:
465
+ tv_info = srcalchemy.collector.deserializeTVInfo(pickleIo=file)
466
+ """
467
+ return pickle.load(pickleIO)
468
+
469
+
470
+ @dataclass
471
+ class MovieInfoStatistics:
472
+ count: int
473
+
474
+
475
+ @dataclass
476
+ class TvInfoStatistics:
477
+ count: int
478
+ episodeCount: int
479
+
480
+
481
+ def movieInfoStatistics(data: list[MovieInfo]) -> MovieInfoStatistics:
482
+ """Get statistics about collected Movies
483
+
484
+ :param data: List of Movies
485
+
486
+ :returns: Statistics
487
+ """
488
+ return MovieInfoStatistics(count=len(data))
489
+
490
+
491
+ def tvInfoStatistics(data: list[TVInfo]) -> TvInfoStatistics:
492
+ """Get statistics about collected Tv Shows
493
+
494
+ :param data: List of Tv Shows
495
+
496
+ :returns: Statistics
497
+ """
498
+
499
+ episode_count = 0
500
+ for tv in data:
501
+ episode_count += len(tv.episodes)
502
+
503
+ return TvInfoStatistics(count=len(data), episodeCount=episode_count)
@@ -0,0 +1,53 @@
1
+ #################################################################################
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2025 Duncan Fraser
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #################################################################################
24
+ #
25
+ # File : __init__.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+
33
+ from .config_dump import writeDefaultConfig
34
+ from .config_load import (
35
+ ConfigSpec,
36
+ driveMappingConfig,
37
+ generalConfig,
38
+ kodiConfig,
39
+ movieConfig,
40
+ parseConfig,
41
+ tvConfig,
42
+ )
43
+
44
+ __all__ = [
45
+ "ConfigSpec",
46
+ "driveMappingConfig",
47
+ "generalConfig",
48
+ "kodiConfig",
49
+ "movieConfig",
50
+ "parseConfig",
51
+ "tvConfig",
52
+ "writeDefaultConfig",
53
+ ]