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 +48 -0
- srcalchemy/__main__.py +36 -0
- srcalchemy/_version.py +1 -0
- srcalchemy/collector/__init__.py +63 -0
- srcalchemy/collector/collector.py +503 -0
- srcalchemy/config/__init__.py +53 -0
- srcalchemy/config/config_dump.py +171 -0
- srcalchemy/config/config_load.py +278 -0
- srcalchemy/process/__init__.py +46 -0
- srcalchemy/process/process.py +346 -0
- srcalchemy/py.typed +0 -0
- srcalchemy/srcalchemy.py +358 -0
- srcalchemy-0.2.0.dist-info/METADATA +105 -0
- srcalchemy-0.2.0.dist-info/RECORD +17 -0
- srcalchemy-0.2.0.dist-info/WHEEL +4 -0
- srcalchemy-0.2.0.dist-info/entry_points.txt +2 -0
- srcalchemy-0.2.0.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|