plexer-cli 0.0.3.post4__py3-none-any.whl → 0.1.2__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.
plexer_cli/const.py CHANGED
@@ -4,4 +4,23 @@ Plexer - Normalize media files for use with Plex Media Server
4
4
  Module: Const - collection of global variables used across the application
5
5
  """
6
6
 
7
- METADATA_FILE_NAME = ".plexer"
7
+ ARTIFACT_NAME_REGEX = r"^(.+) (\([\d]{4}\)) ?(\{edition-.+\})?$"
8
+ ARTIFACT_HEURISTICS_PATTERNS = {
9
+ "name": r"^(.+?)([\.\-\_\(\[][1|2])", # anything before the first instance of commonly-used separators
10
+ "common_editions": r"(?:[Dd]irector(?:')?s|[Ff]inal|[Ee]xtended|[Tt]heatrical|[Uu]nrated)[\.\-\_ ](?:Cut|Edit)|Uncut", # common multi-word edition names (e.g., "Directors Cut", "Theatrical Edit", etc.)
11
+ "edition": r"([A-Za-z0-9']+)([\.\-\_ ]{1})([Ee][Dd][Ii][Tt][Ii][Oo][Nn])|(?:{edition-)(.+)(?:})", # "XXX edition" (case-insensitive) or standard Plex format
12
+ "release_year": r"(19|20)([0-9]{2})", # any 4 digit number between 1900 and 2099
13
+ }
14
+ ARTIFACT_FILE_TYPE_WHITELIST = [
15
+ # Videos
16
+ "video/mp4",
17
+ "video/x-matroska", # .mkv
18
+ "video/x-msvideo" # .avi
19
+ # Subtitles
20
+ "application/x-subrip" # .srt
21
+ "text/plain" # .sub subtitle files
22
+ "text/subtitle" # .sub subtitle files (ALT)
23
+ "application/octet-stream" # .sub index files
24
+ # Metadata
25
+ "text/x-nfo", # .nfo
26
+ ]
@@ -5,12 +5,14 @@ Module: File Manager - code for file-related ops
5
5
  """
6
6
 
7
7
  import os
8
+ import re
9
+
8
10
  from pathlib import Path
9
11
  from magic import from_file
10
12
  from logzero import logger
11
13
 
12
14
  from .artifact import Artifact
13
- from .const import METADATA_FILE_NAME
15
+ from .const import ARTIFACT_NAME_REGEX, ARTIFACT_FILE_TYPE_WHITELIST
14
16
  from .metadata import Metadata
15
17
 
16
18
 
@@ -28,104 +30,222 @@ class FileManager:
28
30
 
29
31
  def get_artifacts(self, tgt_dir="") -> list:
30
32
  """
31
- Gather the names of all files and directories in a given directory and return as list
32
-
33
+ Gather the names of all files and directories in a given directory and return as list.
33
34
 
34
- Target directory is the source directory by default.
35
+ Target directory is the source directory by default, but can be specified via parameter.
35
36
  """
36
37
 
37
38
  artifacts = []
38
39
  tgt_dir = tgt_dir if tgt_dir else self.src_dir
39
40
 
40
41
  with os.scandir(tgt_dir) as sd_iter:
41
- for artifact_entry in sd_iter:
42
+ for dir_artifact in sd_iter:
42
43
  try:
43
- artifact_mime_type = from_file(artifact_entry.path, mime=True)
44
+ artifact_mime_type = from_file(dir_artifact.path, mime=True)
44
45
  except IsADirectoryError:
45
46
  artifact_mime_type = "directory"
46
47
 
47
48
  artifacts.append(
48
49
  Artifact(
49
- name=artifact_entry.name,
50
- path=artifact_entry.path,
50
+ name=dir_artifact.name,
51
+ path=dir_artifact.path,
51
52
  mime_type=artifact_mime_type,
52
53
  )
53
54
  )
54
55
 
55
56
  return artifacts
56
57
 
57
- def prep_artifacts(self, artifacts: list) -> list:
58
+ def check_artifact(self, artifact: Artifact) -> bool:
58
59
  """
59
- Perform any processing needed to prepare the artifact data for further processing
60
+ Perform any checks needed to determine if the artifact is valid for further processing
60
61
 
61
62
  Right now, this includes:
62
- * Properly ordering artifacts such that the metadata file is first
63
+ * Checking if the artifact name is in a valid format required by Plex
63
64
  """
64
65
 
65
- for idx, artifact in enumerate(artifacts):
66
- if artifact.name == METADATA_FILE_NAME:
67
- # float it to the top of the artifact set
68
- artifacts.pop(idx)
69
- artifacts.insert(0, artifact)
66
+ valid_artifact = False
70
67
 
71
- break
68
+ artifact_name_reg = re.compile(ARTIFACT_NAME_REGEX)
69
+ if artifact_name_reg.match(artifact.name):
70
+ logger.debug(f"artifact name format is VALID: {artifact.name}")
72
71
 
73
- return artifacts
72
+ valid_artifact = True
73
+ else:
74
+ logger.debug(f"artifact name format is INVALID: {artifact.name}")
75
+
76
+ return valid_artifact
77
+
78
+ def rename_artifact(
79
+ self, artifact: Artifact, video_metadata: Metadata, dry_run=False
80
+ ) -> Artifact:
81
+ """
82
+ Rename an artifact to the new name generated from the given metadata
74
83
 
75
- def process_directory(self, dir_artifacts: list) -> None:
84
+ Returns the new, updated artifact object
76
85
  """
77
- Traverse the given directory artifacts, rename the
78
- video files accordingly, and delete everything else
79
86
 
80
- NOTE: the artifact for the metadata file MUST come first
81
- in the artifact list. Failing to do so may lead to instability
82
- during artifact processing.
87
+ new_artifact_name = f"{video_metadata.name} ({video_metadata.release_year})"
88
+
89
+ if video_metadata.edition:
90
+ new_artifact_name += f" {{edition-{video_metadata.edition}}}"
91
+
92
+ # get artifact file info for srrc/dst path generation
93
+ artifact_file_path = Path(artifact.absolute_path)
94
+ artifact_parent_dir = artifact_file_path.parent
95
+ artifact_file_ext = (
96
+ "" if artifact.mime_type == "directory" else artifact_file_path.suffix
97
+ )
98
+
99
+ src_file = artifact_file_path.absolute()
100
+ dst_file = f"{artifact_parent_dir}/{new_artifact_name}{artifact_file_ext}"
101
+
102
+ logger.debug(
103
+ f"renaming artifact: [ OLD PATH: {src_file} ] to [ NEW PATH: {dst_file} ]"
104
+ )
105
+
106
+ if src_file != dst_file:
107
+ logger.debug(
108
+ f"executing rename operation on filesystem: {src_file} -> {dst_file}"
109
+ )
110
+ if not dry_run:
111
+ os.rename(src_file, dst_file)
112
+ artifact.name = new_artifact_name
113
+ artifact.absolute_path = dst_file
114
+ else:
115
+ logger.debug("dry run enabled; skipping actual rename operation")
116
+ else:
117
+ logger.debug(
118
+ "source and destination paths are identical; skipping rename operation"
119
+ )
120
+
121
+ return artifact
122
+
123
+ def analyze_artifact(
124
+ self, artifact: Artifact, prompt_behavior: str, dry_run: bool
125
+ ) -> Metadata:
126
+ """
127
+ Analyze the given artifact and return a Metadata object with the relevant info
83
128
  """
84
129
 
130
+ logger.debug(f"starting analysis of artifact: {artifact.name}")
131
+
132
+ # use heuristics to attempt to determine metadata from directory name
85
133
  video_metadata = Metadata()
134
+ video_metadata.metadata_found = False
86
135
 
87
- logger.debug("starting directory artifact processing")
136
+ if video_metadata.do_heuristic_analysis(file_name=artifact.name):
137
+ logger.info(
138
+ f"metadata found for directory via heuristics - name: {video_metadata.name}, release_year: {video_metadata.release_year}",
139
+ )
140
+ video_metadata.metadata_found = True
88
141
 
89
- for artifact in dir_artifacts:
142
+ if prompt_behavior == "all" or (
143
+ prompt_behavior == "default" and not video_metadata.metadata_found
144
+ ):
90
145
  logger.info(
91
- "processing artifact: [ FILE: %s | PATH: %s | FILE TYPE: %s ]",
92
- artifact.name,
93
- artifact.absolute_path,
94
- artifact.mime_type,
146
+ "user requested all prompts OR empty or incomplete metadata found for directory via heuristics; prompting user for manual input"
95
147
  )
148
+ video_metadata.prompt_user_for_metadata()
96
149
 
97
- if artifact.mime_type == "directory":
98
- logger.info("subdirectory found, processing")
150
+ return video_metadata
99
151
 
100
- # start recursive subprocessing
101
- new_dir_artifacts = self.get_artifacts(tgt_dir=artifact.absolute_path)
102
- self.process_directory(dir_artifacts=new_dir_artifacts)
103
- elif artifact.name == METADATA_FILE_NAME:
104
- # read in video metadata
105
- logger.info("metadata file found, importing")
152
+ def process_artifacts(
153
+ self,
154
+ artifacts: list,
155
+ video_metadata=Metadata(),
156
+ prompt_behavior="default",
157
+ dry_run=False,
158
+ ) -> None:
159
+ """
160
+ Traverse the given set of artifacts, rename the video files accordingly, and delete everything else.
106
161
 
107
- video_metadata.import_metadata_from_file(
108
- file_path=artifact.absolute_path
162
+ Subdirectories are processed recursively.
163
+ """
164
+
165
+ logger.debug("starting artifact processing")
166
+
167
+ for artifact in artifacts:
168
+ logger.info(
169
+ f"processing artifact: [ FILE: {artifact.name} | PATH: {artifact.absolute_path} | FILE TYPE: {artifact.mime_type} ]"
170
+ )
171
+
172
+ if artifact.mime_type == "directory":
173
+ logger.info(
174
+ "subdirectory found; assuming media directory and processing accordingly"
109
175
  )
110
- elif artifact.mime_type.startswith("video/"):
111
- # move + rename
112
- logger.info("video file found, renaming")
113
-
114
- file_path = Path(artifact.absolute_path)
115
- file_dir = file_path.parent
116
- file_ext = file_path.suffix
117
-
118
- src_file = artifact.absolute_path
119
- dst_file = (
120
- f"{file_dir}/{video_metadata.name}"
121
- f" ({video_metadata.release_year}){file_ext}"
176
+
177
+ # perform analysis for video metadata
178
+ generated_video_metadata = self.analyze_artifact(
179
+ artifact=artifact,
180
+ prompt_behavior=prompt_behavior,
181
+ dry_run=dry_run,
122
182
  )
123
183
 
124
- logger.debug("moving %s to %s", src_file, dst_file)
184
+ if generated_video_metadata.metadata_found:
185
+ logger.info(
186
+ "analysis successful, metadata found; renaming artifact based on gathered metadata"
187
+ )
188
+
189
+ artifact = self.rename_artifact(
190
+ artifact=artifact,
191
+ video_metadata=generated_video_metadata,
192
+ dry_run=dry_run,
193
+ )
125
194
 
126
- os.rename(src_file, dst_file)
195
+ video_metadata = generated_video_metadata
196
+ else:
197
+ logger.warning(
198
+ "no metadata found for directory after exhausting all methods; skipping artifact processing"
199
+ )
200
+
201
+ continue
202
+
203
+ # once metadata is available, start recursive subprocessing
204
+ new_dir_artifacts = self.get_artifacts(tgt_dir=artifact.absolute_path)
205
+ if new_dir_artifacts:
206
+ self.process_artifacts(
207
+ artifacts=new_dir_artifacts,
208
+ video_metadata=video_metadata,
209
+ prompt_behavior=prompt_behavior,
210
+ dry_run=dry_run,
211
+ )
127
212
  else:
128
- # delete the file
129
- logger.info("unnecessary file found, deleting")
213
+ if not video_metadata.metadata_found:
214
+ raise ValueError(
215
+ f"file artifact found ({artifact.name}) but no video metadata provided; unable to proceed"
216
+ )
217
+
218
+ logger.info(
219
+ f"file artifact found, processing accordingly: {artifact.name}"
220
+ )
221
+
222
+ # delete any files with file-types not whitelisted; rename whitelisted files accordingly
223
+ if artifact.mime_type not in ARTIFACT_FILE_TYPE_WHITELIST:
224
+ logger.warning(
225
+ f"file artifact is not a whitelisted file type; deleting: {artifact.name}"
226
+ )
227
+ if not dry_run:
228
+ os.remove(artifact.absolute_path)
229
+ else:
230
+ logger.debug(
231
+ "dry run enabled; skipping actual delete operation"
232
+ )
233
+ else:
234
+ logger.debug(
235
+ "file artifact is a whitelisted file type", artifact.mime_type
236
+ )
130
237
 
131
- os.remove(artifact.absolute_path)
238
+ if self.check_artifact(artifact=artifact):
239
+ logger.info(
240
+ "file artifact already in a valid format, skipping rename operation"
241
+ )
242
+ else:
243
+ logger.info(
244
+ "file artifact is NOT in a valid format; renaming accordingly"
245
+ )
246
+
247
+ artifact = self.rename_artifact(
248
+ artifact=artifact,
249
+ video_metadata=video_metadata,
250
+ dry_run=dry_run,
251
+ )
plexer_cli/main.py CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env -S uv run
2
2
  """
3
3
  Plexer - Normalize media files for use with Plex Media Server
4
4
 
@@ -6,7 +6,7 @@ Main App Entrypoint
6
6
  """
7
7
 
8
8
  __author__ = "magneticstain"
9
- __version__ = "0.0.3-4"
9
+ __version__ = "0.1.2"
10
10
  __license__ = "MIT"
11
11
 
12
12
  import argparse
@@ -17,6 +17,7 @@ from logzero import logger
17
17
 
18
18
  from plexer_cli.file_manager import FileManager
19
19
 
20
+
20
21
  def fetch_cli_args() -> argparse.Namespace:
21
22
  """Parse CLI arguments passed to the application during startup"""
22
23
 
@@ -31,6 +32,19 @@ def fetch_cli_args() -> argparse.Namespace:
31
32
  parser.add_argument("-s", "--source-dir", action="store", required=True)
32
33
  parser.add_argument("-d", "--destination-dir", action="store", required=True)
33
34
 
35
+ parser.add_argument(
36
+ "--prompt",
37
+ choices=["all", "none", "default"],
38
+ default="default",
39
+ help="Behavior to take in regards to user prompts (e.g. correcting names for overwriting files). all = prompt user for every artifact; none = never prompt - just trust the heuristics; default = prompt for incomplete matches only",
40
+ )
41
+
42
+ parser.add_argument(
43
+ "--dry-run",
44
+ action="store_true",
45
+ help="Perform a trial run with no changes made",
46
+ )
47
+
34
48
  return parser.parse_args()
35
49
 
36
50
 
@@ -39,20 +53,29 @@ def main():
39
53
 
40
54
  cli_args = fetch_cli_args()
41
55
 
42
- # logzero.json(enable=True)
43
- logzero.loglevel(logzero.DEBUG)
44
- logzero.logfile(None)
56
+ # logzero.logfile(None)
57
+ if cli_args.verbose == 1:
58
+ logzero.loglevel(logzero.INFO)
59
+ elif cli_args.verbose >= 2:
60
+ logzero.loglevel(logzero.DEBUG)
61
+ else:
62
+ logzero.loglevel(logzero.WARNING)
45
63
 
46
64
  logger.info("starting Plexer")
47
65
  logger.debug("options: %s", cli_args)
66
+ if cli_args.dry_run:
67
+ logger.info("performing a dry run; NO CHANGES WILL BE MADE")
48
68
 
49
69
  fm = FileManager(src_dir=cli_args.source_dir, dst_dir=cli_args.destination_dir)
50
70
 
51
- # get and prep artifacts for processing
52
- logger.debug("prepping artifacts for processing")
53
- artifacts = fm.prep_artifacts(artifacts=fm.get_artifacts())
54
- logger.info("%d artifact(s) found in source directory", len(artifacts))
55
-
56
71
  logger.info("processing artifacts")
57
- fm.process_directory(dir_artifacts=artifacts)
72
+ fm.process_artifacts(
73
+ artifacts=fm.get_artifacts(),
74
+ prompt_behavior=cli_args.prompt,
75
+ dry_run=cli_args.dry_run,
76
+ )
58
77
  logger.info("artifact processing completed successfully")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
plexer_cli/metadata.py CHANGED
@@ -4,8 +4,11 @@ Plexer - Normalize media files for use with Plex Media Server
4
4
  Module: Metadata - code for analyzing and managing video metadata
5
5
  """
6
6
 
7
- import json
7
+ import re
8
8
  from logzero import logger
9
+ from prompt_toolkit import PromptSession
10
+
11
+ from .const import ARTIFACT_HEURISTICS_PATTERNS
9
12
 
10
13
 
11
14
  class Metadata:
@@ -14,28 +17,120 @@ class Metadata:
14
17
  """
15
18
 
16
19
  name = ""
17
- release_year = 0
20
+ edition = ""
21
+ release_year = 1900
22
+ metadata_found = False
18
23
 
19
- def __init__(self, name="", release_year=1900) -> None:
24
+ def __init__(
25
+ self, name="", edition="", release_year=1900, metadata_found=False
26
+ ) -> None:
20
27
  self.name = name
21
- self.release_year = release_year
28
+ self.edition = edition
29
+ if release_year >= 0:
30
+ self.release_year = release_year
31
+ self.metadata_found = metadata_found
32
+
33
+ def scrub_artifact_name(self, artifact_name: str) -> str:
34
+ """
35
+ Remove common separators and file extensions from given artifact name.
36
+
37
+ Typically used to produce a cleaner string as part of heuristic analysis.
38
+ """
39
+
40
+ logger.debug("scrubbing artifact name: %s", artifact_name)
41
+
42
+ # replace common separators with spaces and strip surrounding whitespace
43
+ scrubbed_name = re.sub(r"[\.\_\-\(\)\[\]]+", " ", artifact_name).strip()
44
+
45
+ logger.debug("artifact name post-scrubbing: %s", scrubbed_name)
46
+
47
+ return scrubbed_name
48
+
49
+ def prompt_user_for_metadata(self) -> None:
50
+ """
51
+ Prompt the user for metadata values via CLI input
52
+ """
53
+
54
+ logger.debug("prompting user for metadata input")
55
+
56
+ prompt_sess = PromptSession()
22
57
 
23
- def import_metadata_from_file(self, file_path: str) -> None:
58
+ user_name = prompt_sess.prompt(
59
+ "Enter the correct name for this media: ", default=self.name
60
+ )
61
+ user_edition = prompt_sess.prompt(
62
+ "Enter the correct edition for this media (optional): ",
63
+ default=self.edition,
64
+ )
65
+ user_release_year = prompt_sess.prompt(
66
+ "Enter the release year for this media: ",
67
+ default=str(
68
+ self.release_year
69
+ ), # `default` argument _must_ be a string, per docs
70
+ )
71
+
72
+ self.name = user_name
73
+ self.edition = user_edition
74
+ self.release_year = int(user_release_year)
75
+ self.metadata_found = True
76
+
77
+ def do_heuristic_analysis(self, file_name: str) -> bool:
24
78
  """
25
- Read in given file and process data into metadata values
79
+ Analyze given file name and attempt to extract metadata values via heuristics
26
80
  """
27
81
 
28
- logger.debug("metadata file found @ %s - importing data", file_path)
82
+ logger.debug("performing heuristic analysis on artifact: %s", file_name)
29
83
 
30
- with open(file_path, mode="r", encoding="utf-8") as metadata_file:
31
- imported_metadata = json.load(metadata_file)
84
+ # perform basic heuristics
85
+ ## NAME
86
+ possible_name = re.search(ARTIFACT_HEURISTICS_PATTERNS["name"], file_name)
87
+ if possible_name:
88
+ self.name = self.scrub_artifact_name(possible_name.group(1))
32
89
 
33
- logger.debug("data imported as: %s", imported_metadata)
90
+ ## EDITION
91
+ # try checking for commonly-used edition names first
92
+ possible_edition = re.search(
93
+ ARTIFACT_HEURISTICS_PATTERNS["common_editions"], file_name
94
+ )
95
+ if possible_edition and possible_edition.group(0):
96
+ self.edition = self.scrub_artifact_name(possible_edition.group(0))
97
+ else:
98
+ # if common edition names aren't found, try using a more generic pattern
99
+ possible_edition = re.search(
100
+ ARTIFACT_HEURISTICS_PATTERNS["edition"], file_name
101
+ )
34
102
 
35
- try:
36
- self.name = imported_metadata["name"]
37
- self.release_year = imported_metadata["release_year"]
38
- except KeyError as e:
39
- logger.error(
40
- 'data missing in metadata file; "%s" field was not found', e.args[0]
103
+ if possible_edition:
104
+ if possible_edition.group(1):
105
+ self.edition = self.scrub_artifact_name(possible_edition.group(1))
106
+ elif possible_edition.group(2):
107
+ self.edition = self.scrub_artifact_name(possible_edition.group(2))
108
+
109
+ ## RELEASE YEAR
110
+ possible_release_year = re.findall(
111
+ ARTIFACT_HEURISTICS_PATTERNS["release_year"], file_name
112
+ )
113
+ if possible_release_year:
114
+ # if multiple years are found, take the last one since it's more likely to be the correct value
115
+ # (e.g. "Movie.Name.1999.1080p.2020" should yield 2020 as the release year, not 1999)
116
+ self.release_year = int(
117
+ "".join(possible_release_year[-1])
118
+ ) # Ex: [('19', '99'), ('20', '20')] -> ('20', '20') -> 2020
119
+
120
+ if possible_name and possible_release_year:
121
+ logger.debug(
122
+ "heuristic analysis results - name: %s, edition: %s, release_year: %d",
123
+ self.name,
124
+ self.edition,
125
+ self.release_year,
41
126
  )
127
+
128
+ self.metadata_found = True
129
+ return True
130
+
131
+ logger.debug("heuristic analysis produced partial or no results")
132
+ self.metadata_found = (
133
+ False # in case it has been overwritten from the default value
134
+ )
135
+
136
+ return False
@@ -1,11 +1,10 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.5
2
2
  Name: plexer_cli
3
- Version: 0.0.3.post4
4
- Summary: Normalize media files for use with Plex Media Server
5
- Project-URL: Repository, https://github.com/magneticstain/plexer.git
6
- Project-URL: Documentation, https://github.com/magneticstain/plexer#readme
3
+ Version: 0.1.2
4
+ Summary: Normalize media files and directories for use with Plex Media Server
5
+ Project-URL: Repository, https://github.com/magneticstain/plexer
6
+ Project-URL: Documentation, https://github.com/magneticstain/plexer-cli/wiki
7
7
  Project-URL: Issues, https://github.com/magneticstain/plexer/issues
8
- Project-URL: Source, https://github.com/magneticstain/plexer
9
8
  Author-email: Josh Carlson <837837+magneticstain@users.noreply.github.com>
10
9
  License-Expression: MIT
11
10
  License-File: LICENSE
@@ -16,16 +15,21 @@ Classifier: Framework :: Hatch
16
15
  Classifier: Framework :: Pytest
17
16
  Classifier: Framework :: tox
18
17
  Classifier: Intended Audience :: System Administrators
19
- Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Natural Language :: English
19
+ Classifier: Operating System :: MacOS
20
20
  Classifier: Operating System :: Unix
21
21
  Classifier: Programming Language :: Python
22
- Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3
23
23
  Classifier: Programming Language :: Python :: 3.10
24
24
  Classifier: Programming Language :: Python :: 3.11
25
25
  Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Topic :: Multimedia :: Video
26
29
  Classifier: Topic :: Utilities
27
- Requires-Python: >=3.9
30
+ Requires-Python: >=3.10
28
31
  Requires-Dist: logzero~=1.7.0
32
+ Requires-Dist: prompt-toolkit>=3.0.52
29
33
  Requires-Dist: python-magic~=0.4.27
30
34
  Description-Content-Type: text/markdown
31
35
 
@@ -41,8 +45,8 @@ Description-Content-Type: text/markdown
41
45
  ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/magneticstain/plexer)
42
46
  ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/magneticstain/plexer/total)
43
47
 
44
- [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/magneticstain/plexer/badge)](https://scorecard.dev/viewer/?uri=github.com/magneticstain/plexer)
45
- [![Codacy Badge](https://app.codacy.com/project/badge/Grade/38b2a65ed9ac4c85afc98e259d73474f)](https://app.codacy.com/gh/magneticstain/plexer/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
48
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/magneticstain/plexer-cli/badge)](https://scorecard.dev/viewer/?uri=github.com/magneticstain/plexer-cli)
49
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13659/badge)](https://www.bestpractices.dev/projects/13659)
46
50
 
47
51
  [![Run Full Suite of Checks and Tests](https://github.com/magneticstain/plexer/actions/workflows/run_full_test_suite.yml/badge.svg)](https://github.com/magneticstain/plexer/actions/workflows/run_full_test_suite.yml)
48
52
  [![Release](https://github.com/magneticstain/plexer/actions/workflows/release.yml/badge.svg)](https://github.com/magneticstain/plexer/actions/workflows/release.yml)
@@ -60,15 +64,13 @@ Currently, Plexer only supports organizing movies and other individual video fil
60
64
  ### Roadmap
61
65
 
62
66
  - [ ] Support for TV Shows ([#28](https://github.com/magneticstain/plexer/issues/28))
63
- - [ ] Docker implementations ([#29](https://github.com/magneticstain/plexer/issues/29))
67
+ - [X] Docker implementations ([#29](https://github.com/magneticstain/plexer/issues/29))
64
68
  - [ ] MacOS support ([#30](https://github.com/magneticstain/plexer/issues/30))
65
69
  - [ ] Windows support ([#31](https://github.com/magneticstain/plexer/issues/31))
66
- - [ ] Metadata inference using file/directory info (i.e. analyze the video files to "guess" what the name, year, etc is) ([#32](https://github.com/magneticstain/plexer/issues/32))
70
+ - [X] Metadata inference using file/directory info (i.e. analyze the video files to "guess" what the name, year, etc is) ([#32](https://github.com/magneticstain/plexer/issues/32))
67
71
 
68
72
  ## Install
69
73
 
70
- To install Plexer, there are a few options available.
71
-
72
74
  ### Docker
73
75
 
74
76
  The most portable way to install and run Plexer is by using Docker.
@@ -89,7 +91,7 @@ docker run --rm -it magneticstain/plexer-cli
89
91
  docker run --rm -it ghcr.io/magneticstain/plexer-cli
90
92
  ```
91
93
 
92
- #### Via Local Build
94
+ #### Via Local Image Build
93
95
 
94
96
  In the case that container registries are unavailable, there's also the option to build the image locally. To do that, check out the `main` branch of this repo, build the Plexer image, and run it.
95
97
 
@@ -101,21 +103,19 @@ docker run --rm -it plexer_cli
101
103
 
102
104
  ## Requirements
103
105
 
104
- ### Media Metadata
106
+ ### Linux and MacOS Native Install
105
107
 
106
- The most important requirement before running plexer is to ensure that you've created a `.plexer` file in each of your target directories.
108
+ If you've installed plexer-cli natively (i.e. outside of a Docker container) on Ubuntu, Devian, Fedora, etc, or MacOS, you will also need to install additional dependencies:
107
109
 
108
- This is a JSON-formatted file that includes the movie metadata required by Plexer to perform its jobs.
110
+ #### Python-Magic
109
111
 
110
- #### Plexer File Generator
112
+ While `python-magic` is imported in the standard fashion (via pip/python requirements), it itself depends on the presence of the libmagic C library. To install this library, follow [the installation instructions provided in the `python-magic` README](https://github.com/ahupp/python-magic?tab=readme-ov-file#installation).
111
113
 
112
- To easily create the `.plexer` file, you can use the one-liner below while in the movie's directory:
114
+ ### Media Metadata
113
115
 
114
- ```bash
115
- echo -n "Media Name: ";read MEDIA_NAME;echo -n "Release Year (YYYY): ";read RELEASE_YEAR;echo "{\"name\": \"${MEDIA_NAME}\", \"release_year\": \"${RELEASE_YEAR}\"}" > .plexer
116
- ```
116
+ The most important requirement before running plexer is to ensure that you've created a `.plexer` file in each of your target directories.
117
117
 
118
- It can be modified to support different types of media as well.
118
+ This is a JSON-formatted file that includes the movie metadata required by Plexer to perform its jobs.
119
119
 
120
120
  ## Usage
121
121
 
@@ -149,7 +149,17 @@ For developing with Plexer, there are several tools that are in use:
149
149
  1. [Hatch](https://hatch.pypa.io/1.12/)
150
150
  1. Analysis Tools:
151
151
  1. [Ruff](https://docs.astral.sh/ruff/)
152
- 1. [Codacy](https://app.codacy.com/gh/magneticstain/plexer/dashboard)
153
152
  1. Testing:
154
153
  1. [Pytest](https://docs.pytest.org/en/latest/)
155
154
  1. [Tox](https://tox.wiki/en/stable/)
155
+
156
+ ### Coding Standards and Conventions
157
+
158
+ - Pre-commit hooks are utilized and managed in `.pre-commit-config.yml`
159
+ - For example, `ruff` is run as part of every commit
160
+ - These pre-commit hooks **MUST** be installed and utilized by all developers
161
+
162
+ #### Testing
163
+
164
+ - All functions **MUST** have unit tests created for them
165
+ - Tests are executed using **VS Code** with the `launch.json` config file in `.vscode`
@@ -0,0 +1,11 @@
1
+ plexer_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ plexer_cli/artifact.py,sha256=JnaWeEM9msrs_EZpfGn7Xr1TcG8A7mUG-7BXuKNwXdU,378
3
+ plexer_cli/const.py,sha256=fr7dfKuovYv5N7lo0sGgsDP59pmWDRdjktDWUTmLGB4,1169
4
+ plexer_cli/file_manager.py,sha256=-1iV6UPnmwV1JJ5gTUrSz99O-Bm4zh2ljy6n2xX9wcI,9026
5
+ plexer_cli/main.py,sha256=skc4OrYoSyG3ovqREjF7tIm7pQbJ3nrPjec29GpUtx0,2306
6
+ plexer_cli/metadata.py,sha256=xot4tLGd_eAvaZcb17EY8w7LaNo7m2CUis5YSJEbHEw,4681
7
+ plexer_cli-0.1.2.dist-info/METADATA,sha256=zOM_JQHmbf1OBwiy_Bh657l3uEEipPE-mZczOPCqw-k,7324
8
+ plexer_cli-0.1.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ plexer_cli-0.1.2.dist-info/entry_points.txt,sha256=0kHe-oXDezw_oNI1nB5LLLlFwMIhGN4QNv3h-mWWPHc,48
10
+ plexer_cli-0.1.2.dist-info/licenses/LICENSE,sha256=cs3weOTez4n04m4foiWXOU6C6Pr83ORocewlbPDaJGs,1069
11
+ plexer_cli-0.1.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,11 +0,0 @@
1
- plexer_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- plexer_cli/artifact.py,sha256=JnaWeEM9msrs_EZpfGn7Xr1TcG8A7mUG-7BXuKNwXdU,378
3
- plexer_cli/const.py,sha256=U49nn11PvkOcvUf5eg1_hsYs12ULg_f2VtL81hWje-w,178
4
- plexer_cli/file_manager.py,sha256=nfmBnQXK8JWPp4INfLE6kVqeD6NMxFnn1YoTmywI4Dg,4153
5
- plexer_cli/main.py,sha256=HEvxX4g047KB4yM-719ZK1UR6mdciFvdMo5SLwIqeJ8,1687
6
- plexer_cli/metadata.py,sha256=UQ0nazq3agv7-h4NiVHNlpf2z_6rPLCBQ3TfIXKn0F8,1170
7
- plexer_cli-0.0.3.post4.dist-info/METADATA,sha256=Ou4zAgwRaPp4O9Kt_PCDWRCrvBR-_kX_wF-WaEQWk6M,6806
8
- plexer_cli-0.0.3.post4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
9
- plexer_cli-0.0.3.post4.dist-info/entry_points.txt,sha256=0kHe-oXDezw_oNI1nB5LLLlFwMIhGN4QNv3h-mWWPHc,48
10
- plexer_cli-0.0.3.post4.dist-info/licenses/LICENSE,sha256=cs3weOTez4n04m4foiWXOU6C6Pr83ORocewlbPDaJGs,1069
11
- plexer_cli-0.0.3.post4.dist-info/RECORD,,