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.
@@ -0,0 +1,346 @@
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 : process.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+
33
+ import os
34
+ import shutil
35
+ from dataclasses import dataclass
36
+ from typing import Literal
37
+
38
+ from srcalchemy.collector import EpisodeInfo, MovieInfo, TVInfo, fileInfo, parseFileInfo
39
+ from srcalchemy.config import ConfigSpec, movieConfig, tvConfig
40
+
41
+
42
+ def formatMovieParent(movie_info: MovieInfo, movie_config: movieConfig) -> str:
43
+ """Generate the Parent Path for storing Movie
44
+
45
+ :param movie_info: Movie Information
46
+ :param movie_config: Configuration Options for Movies from Base Configuration
47
+
48
+ :returns: Directory Name
49
+ """
50
+ format_map = {
51
+ "TITLE": movie_info.title,
52
+ "YEAR": movie_info.year,
53
+ "FORMAT": movie_info.formatString,
54
+ }
55
+
56
+ movie_parent = movie_config.parentFormat.format_map(format_map)
57
+
58
+ return movie_parent
59
+
60
+
61
+ def formatMovieFile(movie_info: MovieInfo, movie_config: movieConfig) -> str:
62
+ """Generate the file name for storing Movie
63
+
64
+ :param movie_info: Movie Information
65
+ :param movie_config: Configuration Options for Movies from Base Configuration
66
+
67
+ :returns: Filename
68
+ """
69
+ format_map = {
70
+ "TITLE": movie_info.title,
71
+ "YEAR": movie_info.year,
72
+ "FORMAT": movie_info.formatString,
73
+ }
74
+
75
+ movie_file = movie_config.fileFormat.format_map(format_map)
76
+
77
+ return movie_file
78
+
79
+
80
+ def formatTvParent(
81
+ tv_info: TVInfo, episode_info: EpisodeInfo, tv_config: tvConfig
82
+ ) -> str:
83
+ """Generate the Parent Path for storing Episodes
84
+
85
+ :param tv_info: TV Show Information
86
+ :param episode_info: Episode Information
87
+ :param tv_config: Configuration Options for TV Shows from Base Configuration
88
+
89
+ :returns: Directory Name
90
+ """
91
+ format_map = {
92
+ "TITLE": tv_info.title,
93
+ "YEAR": tv_info.year,
94
+ }
95
+
96
+ tv_parent = tv_config.parentFormat.format_map(format_map)
97
+
98
+ return tv_parent
99
+
100
+
101
+ def formatTvSeason(
102
+ tv_info: TVInfo, episode_info: EpisodeInfo, tv_config: tvConfig
103
+ ) -> str:
104
+ """Generate the Season Path for storing Episodes
105
+
106
+ :param tv_info: TV Show Information
107
+ :param episode_info: Episode Information
108
+ :param tv_config: Configuration Options for TV Shows from Base Configuration
109
+
110
+ :returns: Directory Name
111
+ """
112
+ format_map = {
113
+ "TITLE": tv_info.title,
114
+ "YEAR": tv_info.year,
115
+ "SEASON": episode_info.season,
116
+ }
117
+
118
+ tv_season = tv_config.seasonFormat.format_map(format_map)
119
+
120
+ return tv_season
121
+
122
+
123
+ def formatTvEpisode(
124
+ tv_info: TVInfo, episode_info: EpisodeInfo, tv_config: tvConfig
125
+ ) -> str:
126
+ """Generate the file name for storing Episodes
127
+
128
+ :param tv_info: TV Show Information
129
+ :param episode_info: Episode Information
130
+ :param tv_config: Configuration Options for TV Shows from Base Configuration
131
+
132
+ :returns: Filename
133
+ """
134
+ format_map = {
135
+ "SHOW_TITLE": tv_info.title,
136
+ "TITLE": episode_info.title,
137
+ "YEAR": tv_info.year,
138
+ "SEASON": episode_info.season,
139
+ "EPISODE": episode_info.episode,
140
+ }
141
+
142
+ tv_episode = tv_config.fileFormat.format_map(format_map)
143
+
144
+ return tv_episode
145
+
146
+
147
+ @dataclass
148
+ class renameOperation:
149
+ """Rename Operation
150
+
151
+ Stores the source and destination and provides a status string
152
+ """
153
+
154
+ src: fileInfo
155
+ """Source file information"""
156
+ dest: fileInfo
157
+ """Destination file information"""
158
+ status: Literal["ok", "warning", "fail", "skip"] | None = None
159
+ """Status of operation"""
160
+
161
+
162
+ def performRenameOperation(
163
+ config: ConfigSpec, data: renameOperation, dry_run: bool = False
164
+ ) -> renameOperation:
165
+ """Perform Rename Operation
166
+
167
+ Will rename the source file path to the destination file path and create the path if it does not exist.
168
+ The rename will not be performed if the source and destination file path are the same.
169
+ If the mode is "copy" will copy the file before renaming. If the mode is "move" will move and rename.
170
+
171
+ :param config: Configuration object
172
+ :param data: Rename operation to perform
173
+
174
+ :returns: Rename Operation
175
+ """
176
+ if data.src == data.dest:
177
+ data.status = "skip"
178
+ config.log.debug(
179
+ f"Source and Destination are the same path : {data.src.filename}"
180
+ )
181
+ return data
182
+
183
+ if not os.path.exists(data.src.path):
184
+ config.log.debug(f"Source does not exist : {data.src.path}")
185
+ data.status = "fail"
186
+ return data
187
+
188
+ if not os.path.isdir(data.dest.parentPath):
189
+ os.makedirs(data.dest.parentPath, exist_ok=True)
190
+
191
+ if not dry_run:
192
+ if config.general.mode == "move":
193
+ shutil.move(src=data.src.path, dst=data.dest.path)
194
+ elif config.general.mode == "copy":
195
+ shutil.copy(src=data.src.path, dst=data.dest.path)
196
+
197
+ data.status = "ok"
198
+ return data
199
+
200
+
201
+ def performCleanDirectory(
202
+ config: ConfigSpec, data: renameOperation, dry_run: bool = False
203
+ ) -> bool:
204
+ """Clean Source Directory
205
+
206
+ Delete parent path of source file information.
207
+ Will not be performed if the destination parent path is the same as the source parent path
208
+
209
+ :param config: Configuration object
210
+ :param data: Rename Operation to delete src parent path
211
+ """
212
+ if not os.path.isdir(data.src.parentPath):
213
+ config.log.debug(f"Parent Path does not exit: {data.src.parentPath}")
214
+ return False
215
+
216
+ if data.src.parentPath == data.src.basePath:
217
+ config.log.debug(f"Parent Path and Base Path are the same: {data.src.basePath}")
218
+ return False
219
+
220
+ if data.src.parentPath == data.dest.parentPath:
221
+ config.log.debug(
222
+ f"Source and Destination Parent Path are the same: {data.src.parentPath}"
223
+ )
224
+ return False
225
+
226
+ if not dry_run:
227
+ shutil.rmtree(path=data.src.parentPath)
228
+ else:
229
+ config.log.debug(f"Dry Run Would remove: {data.src.parentPath}")
230
+
231
+ return True
232
+
233
+
234
+ def processMovieRename(config: ConfigSpec, movie_info: MovieInfo) -> fileInfo:
235
+ """Process Movie Information into Output File Information
236
+
237
+ :param config: Configuration object
238
+ :param movie_info: Movie Information
239
+
240
+ :returns: File information for output file
241
+ """
242
+ new_parent_path = formatMovieParent(
243
+ movie_info=movie_info, movie_config=config.movie_config
244
+ )
245
+ new_filename = formatMovieFile(
246
+ movie_info=movie_info, movie_config=config.movie_config
247
+ )
248
+
249
+ if config.movie_config.outputDirectory is not None:
250
+ new_path = os.path.join(
251
+ config.movie_config.outputDirectory,
252
+ new_parent_path,
253
+ new_filename + movie_info.file.extension,
254
+ )
255
+ base_path = config.movie_config.outputDirectory
256
+ else:
257
+ new_path = os.path.join(
258
+ config.movie_config.basePath,
259
+ new_parent_path,
260
+ new_filename + movie_info.file.extension,
261
+ )
262
+ base_path = config.movie_config.basePath
263
+
264
+ file_info = parseFileInfo(path=new_path, base_path=base_path)
265
+
266
+ return file_info
267
+
268
+
269
+ def processTvRename(
270
+ config: ConfigSpec, tv_info: TVInfo, episode_info: EpisodeInfo
271
+ ) -> fileInfo:
272
+ """Process Episodes Information into Output File Information
273
+
274
+ :param config: Configuration object
275
+ :param tv_info: TV information
276
+ :param episode_info: Episode information
277
+
278
+ :returns: File information for output file
279
+ """
280
+ new_parent_path = formatTvParent(
281
+ tv_info=tv_info, episode_info=episode_info, tv_config=config.tv_config
282
+ )
283
+ new_season_path = formatTvSeason(
284
+ tv_info=tv_info, episode_info=episode_info, tv_config=config.tv_config
285
+ )
286
+ new_filename = formatTvEpisode(
287
+ tv_info=tv_info, episode_info=episode_info, tv_config=config.tv_config
288
+ )
289
+
290
+ if config.tv_config.outputDirectory is not None:
291
+ new_path = os.path.join(
292
+ config.tv_config.outputDirectory,
293
+ new_parent_path,
294
+ new_season_path,
295
+ new_filename + episode_info.file.extension,
296
+ )
297
+ base_path = config.tv_config.outputDirectory
298
+ else:
299
+ new_path = os.path.join(
300
+ config.tv_config.basePath,
301
+ new_parent_path,
302
+ new_season_path,
303
+ new_filename + episode_info.file.extension,
304
+ )
305
+ base_path = config.tv_config.basePath
306
+
307
+ file_info = parseFileInfo(path=new_path, base_path=base_path)
308
+
309
+ return file_info
310
+
311
+
312
+ def collectMovieRename(
313
+ config: ConfigSpec, movie_list: list[MovieInfo]
314
+ ) -> list[renameOperation]:
315
+ """Collect Movie Rename Operations from Movie Information
316
+
317
+ :param config: Configuration object
318
+ :param movie_list: List of Movie Info
319
+
320
+ :returns: List of rename operations to be performed
321
+ """
322
+ file_list: list[renameOperation] = []
323
+ for movie in movie_list:
324
+ new_file_info = processMovieRename(config=config, movie_info=movie)
325
+ file_list.append(renameOperation(src=movie.file, dest=new_file_info))
326
+
327
+ return file_list
328
+
329
+
330
+ def collectTvRename(config: ConfigSpec, tv_info: list[TVInfo]) -> list[renameOperation]:
331
+ """Collect TV Rename Operations from Movie Information
332
+
333
+ :param config: Configuration object
334
+ :param tv_info: List of TV info
335
+
336
+ :returns: List of rename operations to be performed
337
+ """
338
+ file_list: list[renameOperation] = []
339
+ for tv in tv_info:
340
+ for episode in tv.episodes:
341
+ new_file_info = processTvRename(
342
+ config=config, tv_info=tv, episode_info=episode
343
+ )
344
+ file_list.append(renameOperation(src=episode.file, dest=new_file_info))
345
+
346
+ return file_list
srcalchemy/py.typed ADDED
File without changes
@@ -0,0 +1,358 @@
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 : srcalchemy.py
26
+ #
27
+ # Author : Duncan Fraser <dfraser@thedrunkencoder.uk>
28
+ #
29
+ # Description :
30
+ #
31
+ #################################################################################
32
+ import logging
33
+ import os
34
+ import sys
35
+ from argparse import ArgumentParser, Namespace
36
+ from typing import Final
37
+
38
+ from alive_progress import alive_bar
39
+
40
+ from . import _version, collector, config, process
41
+
42
+ DEFAULT_CONFIG_FILE: Final = "config.ini"
43
+ OUTPUT_MOVIES: Final = "movie.bin"
44
+ OUTPUT_TV: Final = "tv.bin"
45
+
46
+
47
+ def collectConfig(config_path: str = DEFAULT_CONFIG_FILE) -> config.ConfigSpec:
48
+ """Parse Configuration from file. If file does not exist returns default configuration
49
+
50
+ :param config_path: Path to configuration file
51
+
52
+ :returns: Populated configuration
53
+ """
54
+ config_data = config.ConfigSpec()
55
+
56
+ if not os.path.exists(config_path):
57
+ return config_data
58
+
59
+ with open(config_path, "rb") as conf_file:
60
+ config_data = config.parseConfig(configIO=conf_file)
61
+
62
+ return config_data
63
+
64
+
65
+ def configLogger(
66
+ level: str, log_file: str | None = None, log_std: bool = False
67
+ ) -> logging.Logger:
68
+ """Configure logging object
69
+
70
+ :param level: String representation of log level
71
+ :param log_file: Path to log file
72
+ :param log_std: If True output log to standard output
73
+
74
+ :returns: Logging Object
75
+ """
76
+ logger = logging.getLogger(__name__)
77
+ form = logging.Formatter(
78
+ "%(asctime)s %(levelname)s [%(module)s:%(funcName)s] %(message)s",
79
+ datefmt="%Y-%m-%d %H:%M:%S",
80
+ )
81
+
82
+ log_levels = {
83
+ "debug": logging.DEBUG,
84
+ "info": logging.INFO,
85
+ "warning": logging.WARNING,
86
+ "error": logging.ERROR,
87
+ "critical": logging.CRITICAL,
88
+ }
89
+
90
+ if level not in log_levels:
91
+ raise ValueError(f"Undefined Level {level}")
92
+
93
+ logger.setLevel(log_levels[level])
94
+
95
+ if log_file:
96
+ fh = logging.FileHandler(filename=log_file, mode="a")
97
+ fh.setFormatter(form)
98
+ logger.addHandler(fh)
99
+
100
+ if log_std:
101
+ sh = logging.StreamHandler(sys.stdout)
102
+ sh.setFormatter(form)
103
+ logger.addHandler(sh)
104
+
105
+ return logger
106
+
107
+
108
+ def parseArgs(config_data: config.ConfigSpec) -> tuple[Namespace, config.ConfigSpec]:
109
+ """Parse command line arguments
110
+
111
+ :params config_data: Configuration data, will be modified and returned
112
+
113
+ :returns: Tuple containing parsed arguments and modified configuration information
114
+ """
115
+ desc_str = f"srcalchemy v{_version.__version__}: Utility for renaming video files from a kodi library"
116
+ parser = ArgumentParser(prog="srcalchemy", description=desc_str)
117
+
118
+ parser.add_argument("--config", "-c", required=False, default=DEFAULT_CONFIG_FILE)
119
+ parser.add_argument(
120
+ "--debug",
121
+ "-d",
122
+ action="store",
123
+ default="info",
124
+ choices=["debug", "info", "warning", "error", "critical"],
125
+ required=False,
126
+ )
127
+ parser.add_argument("--logfile", action="store", required=False, metavar="xxx.log")
128
+ parser.add_argument(
129
+ "--quiet", "-q", action="store_true", required=False, default=False
130
+ )
131
+
132
+ subparsers = parser.add_subparsers(dest="command", required=True)
133
+
134
+ init = subparsers.add_parser("init", help="Initialize config file") # noqa: F841
135
+ collection = subparsers.add_parser( # noqa: F841
136
+ "collect", help="Collect Information"
137
+ )
138
+ rename = subparsers.add_parser("rename", help="Use Information")
139
+ clean = subparsers.add_parser("clean", help="Print Information")
140
+
141
+ stats = subparsers.add_parser("stats", help="Shows collections statistics") # noqa: F841
142
+
143
+ rename.add_argument("--dryRun", required=False, action="store_true")
144
+ clean.add_argument("--dryRun", required=False, action="store_true")
145
+
146
+ args = parser.parse_args()
147
+
148
+ if args.config:
149
+ config_data = collectConfig(args.config)
150
+
151
+ if args.logfile:
152
+ config_data.general.logFile = args.logfile
153
+
154
+ return (args, config_data)
155
+
156
+
157
+ def init(config_path: str = DEFAULT_CONFIG_FILE) -> int:
158
+ """Write default configuration to file
159
+
160
+ :param config_path: Path to configuration file
161
+
162
+ :returns: Status Code
163
+ """
164
+ with open(config_path, "w") as conf_file:
165
+ config.writeDefaultConfig(configIO=conf_file)
166
+ return 0
167
+
168
+
169
+ def collect(config_data: config.ConfigSpec) -> int:
170
+ """Collect Move and TV information and serialize
171
+
172
+ :param config: Configuration object
173
+
174
+ :returns: Status Code
175
+ """
176
+ movie_info = collector.getMovieInfo(config=config_data)
177
+ tv_info = collector.getTvInfo(config=config_data)
178
+
179
+ if not os.path.isdir(config_data.general.dataDirectory):
180
+ os.makedirs(config_data.general.dataDirectory, exist_ok=True)
181
+
182
+ with open(
183
+ os.path.join(config_data.general.dataDirectory, OUTPUT_MOVIES), "wb"
184
+ ) as movie_file:
185
+ collector.serializeMovieInfo(pickleIO=movie_file, movieInfo=movie_info)
186
+
187
+ with open(
188
+ os.path.join(config_data.general.dataDirectory, OUTPUT_TV), "wb"
189
+ ) as tv_file:
190
+ collector.serializeTVInfo(pickleIO=tv_file, tvInfo=tv_info)
191
+
192
+ movie_stats = collector.movieInfoStatistics(data=movie_info)
193
+ tv_stats = collector.tvInfoStatistics(data=tv_info)
194
+
195
+ config_data.log.info(
196
+ "Collection complete. Found %s Movies and %s Episodes over %s Tv Shows",
197
+ movie_stats.count,
198
+ tv_stats.episodeCount,
199
+ tv_stats.count,
200
+ )
201
+ return 0
202
+
203
+
204
+ def rename(config_data: config.ConfigSpec, dry_run: bool = False) -> int:
205
+ """Perform rename operations from serialized data
206
+
207
+ :param config: Configuration object
208
+ :param dry_run: If True, will perform as dry run of rename operation
209
+
210
+ :returns: Status Code
211
+ """
212
+ with open(
213
+ os.path.join(config_data.general.dataDirectory, OUTPUT_MOVIES), "rb"
214
+ ) as movie_file:
215
+ movie_info = collector.deserializeMovieInfo(pickleIO=movie_file)
216
+
217
+ with open(
218
+ os.path.join(config_data.general.dataDirectory, OUTPUT_TV), "rb"
219
+ ) as tv_file:
220
+ tv_info = collector.deserializeTVInfo(pickleIO=tv_file)
221
+
222
+ movie_rename = process.collectMovieRename(config=config_data, movie_list=movie_info)
223
+ tv_rename = process.collectTvRename(config=config_data, tv_info=tv_info)
224
+
225
+ with alive_bar(len(movie_rename), title="Movie Rename") as progress:
226
+ for movie in movie_rename:
227
+ config_data.log.info(
228
+ "{} Movie : {} --> {}".format(
229
+ "Moving" if config_data.general.mode == "move" else "Copying",
230
+ movie.src.path,
231
+ movie.dest.path,
232
+ )
233
+ )
234
+ process.performRenameOperation(
235
+ config=config_data, data=movie, dry_run=dry_run
236
+ )
237
+ progress()
238
+
239
+ with alive_bar(len(tv_rename), title="TV Rename ") as progress:
240
+ for tv in tv_rename:
241
+ config_data.log.info(
242
+ "{} Episode : {} --> {}".format(
243
+ "Moving" if config_data.general.mode == "move" else "Copying",
244
+ tv.src.path,
245
+ tv.dest.path,
246
+ )
247
+ )
248
+
249
+ process.performRenameOperation(config=config_data, data=tv, dry_run=dry_run)
250
+ progress()
251
+
252
+ return 0
253
+
254
+
255
+ def clean(config_data: config.ConfigSpec, dry_run: bool = False) -> int:
256
+ """Perform clean operation
257
+
258
+ :param config: Configuration object
259
+ :param dry_run: If True, will perform as dry run of clean operation
260
+
261
+ :returns: Status Code
262
+ """
263
+ with open(
264
+ os.path.join(config_data.general.dataDirectory, OUTPUT_MOVIES), "rb"
265
+ ) as movie_file:
266
+ movie_info = collector.deserializeMovieInfo(pickleIO=movie_file)
267
+
268
+ with open(
269
+ os.path.join(config_data.general.dataDirectory, OUTPUT_TV), "rb"
270
+ ) as tv_file:
271
+ tv_info = collector.deserializeTVInfo(pickleIO=tv_file)
272
+
273
+ movie_rename = process.collectMovieRename(config=config_data, movie_list=movie_info)
274
+ tv_rename = process.collectTvRename(config=config_data, tv_info=tv_info)
275
+
276
+ with alive_bar(len(movie_rename), title="Movie Clean") as progress:
277
+ for movie in movie_rename:
278
+ config_data.log.info(f"Deleting Movie Directory : {movie.src.parentPath}")
279
+ process.performCleanDirectory(
280
+ config=config_data, data=movie, dry_run=dry_run
281
+ )
282
+ progress()
283
+
284
+ with alive_bar(len(tv_rename), title="TV Clean ") as progress:
285
+ for tv in tv_rename:
286
+ config_data.log.info(f"Deleting TV Directory : {tv.src.parentPath}")
287
+ process.performCleanDirectory(config=config_data, data=tv, dry_run=dry_run)
288
+ progress()
289
+ return 0
290
+
291
+
292
+ def stats(config_data: config.ConfigSpec) -> int:
293
+ """Perform clean operation
294
+
295
+ :param config: Configuration object
296
+
297
+ :returns: Status Code
298
+ """
299
+ with open(
300
+ os.path.join(config_data.general.dataDirectory, OUTPUT_MOVIES), "rb"
301
+ ) as movie_file:
302
+ movie_info = collector.deserializeMovieInfo(pickleIO=movie_file)
303
+
304
+ with open(
305
+ os.path.join(config_data.general.dataDirectory, OUTPUT_TV), "rb"
306
+ ) as tv_file:
307
+ tv_info = collector.deserializeTVInfo(pickleIO=tv_file)
308
+
309
+ movie_stats = collector.movieInfoStatistics(data=movie_info)
310
+ tv_stats = collector.tvInfoStatistics(data=tv_info)
311
+
312
+ config_data.log.info(
313
+ "%s Movies and %s Episodes over %s Tv Shows",
314
+ movie_stats.count,
315
+ tv_stats.episodeCount,
316
+ tv_stats.count,
317
+ )
318
+ return 0
319
+
320
+
321
+ def main() -> int:
322
+ """Main process"""
323
+ config_data = collectConfig(DEFAULT_CONFIG_FILE)
324
+
325
+ args, config_data = parseArgs(config_data=config_data)
326
+
327
+ if not os.path.isdir(config_data.general.dataDirectory):
328
+ os.makedirs(config_data.general.dataDirectory, exist_ok=True)
329
+
330
+ if (
331
+ not os.path.isdir(os.path.dirname(config_data.general.logFile))
332
+ and os.path.dirname(config_data.general.logFile) != ""
333
+ ):
334
+ os.makedirs(os.path.dirname(config_data.general.logFile), exist_ok=True)
335
+
336
+ logger = configLogger(
337
+ level=args.debug, log_file=config_data.general.logFile, log_std=(not args.quiet)
338
+ )
339
+ config_data.log = logger
340
+
341
+ status = 0
342
+
343
+ if args.command == "init":
344
+ status = init(args.config)
345
+ elif args.command == "collect":
346
+ status = collect(config_data=config_data)
347
+ elif args.command == "rename":
348
+ status = rename(config_data=config_data, dry_run=args.dryRun)
349
+ elif args.command == "clean":
350
+ status = clean(config_data=config_data, dry_run=args.dryRun)
351
+ elif args.command == "stats":
352
+ status = stats(config_data=config_data)
353
+
354
+ return status
355
+
356
+
357
+ if __name__ == "__main__":
358
+ sys.exit(main())