plexer-cli 0.0.1a0__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/__init__.py +0 -0
- plexer/artifact.py +20 -0
- plexer/const.py +7 -0
- plexer/file_manager.py +131 -0
- plexer/metadata.py +41 -0
- plexer_cli-0.0.1a0.dist-info/METADATA +105 -0
- plexer_cli-0.0.1a0.dist-info/RECORD +9 -0
- plexer_cli-0.0.1a0.dist-info/WHEEL +4 -0
- plexer_cli-0.0.1a0.dist-info/licenses/LICENSE +21 -0
plexer/__init__.py
ADDED
|
File without changes
|
plexer/artifact.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plexer - Normalize media files for use with Plex Media Server
|
|
3
|
+
|
|
4
|
+
Module: Artifact Class
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Artifact:
|
|
9
|
+
"""
|
|
10
|
+
General artifact object
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
name = ""
|
|
14
|
+
absolute_path = ""
|
|
15
|
+
mime_type = ""
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: str, path: str, mime_type: str) -> None:
|
|
18
|
+
self.name = name
|
|
19
|
+
self.absolute_path = path
|
|
20
|
+
self.mime_type = mime_type
|
plexer/const.py
ADDED
plexer/file_manager.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plexer - Normalize media files for use with Plex Media Server
|
|
3
|
+
|
|
4
|
+
Module: File Manager - code for file-related ops
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from magic import from_file
|
|
10
|
+
from logzero import logger
|
|
11
|
+
|
|
12
|
+
from .artifact import Artifact
|
|
13
|
+
from .const import METADATA_FILE_NAME
|
|
14
|
+
from .metadata import Metadata
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileManager:
|
|
18
|
+
"""
|
|
19
|
+
Class used for any file-related ops
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
src_dir = ""
|
|
23
|
+
dst_dir = ""
|
|
24
|
+
|
|
25
|
+
def __init__(self, src_dir, dst_dir) -> None:
|
|
26
|
+
self.src_dir = src_dir
|
|
27
|
+
self.dst_dir = dst_dir
|
|
28
|
+
|
|
29
|
+
def get_artifacts(self, tgt_dir="") -> list:
|
|
30
|
+
"""
|
|
31
|
+
Gather the names of all files and directories in a given directory and return as list
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Target directory is the source directory by default.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
artifacts = []
|
|
38
|
+
tgt_dir = tgt_dir if tgt_dir else self.src_dir
|
|
39
|
+
|
|
40
|
+
with os.scandir(tgt_dir) as sd_iter:
|
|
41
|
+
for artifact_entry in sd_iter:
|
|
42
|
+
try:
|
|
43
|
+
artifact_mime_type = from_file(artifact_entry.path, mime=True)
|
|
44
|
+
except IsADirectoryError:
|
|
45
|
+
artifact_mime_type = "directory"
|
|
46
|
+
|
|
47
|
+
artifacts.append(
|
|
48
|
+
Artifact(
|
|
49
|
+
name=artifact_entry.name,
|
|
50
|
+
path=artifact_entry.path,
|
|
51
|
+
mime_type=artifact_mime_type,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return artifacts
|
|
56
|
+
|
|
57
|
+
def prep_artifacts(self, artifacts: list) -> list:
|
|
58
|
+
"""
|
|
59
|
+
Perform any processing needed to prepare the artifact data for further processing
|
|
60
|
+
|
|
61
|
+
Right now, this includes:
|
|
62
|
+
* Properly ordering artifacts such that the metadata file is first
|
|
63
|
+
"""
|
|
64
|
+
|
|
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)
|
|
70
|
+
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return artifacts
|
|
74
|
+
|
|
75
|
+
def process_directory(self, dir_artifacts: list) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Traverse the given directory artifacts, rename the
|
|
78
|
+
video files accordingly, and delete everything else
|
|
79
|
+
|
|
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.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
video_metadata = Metadata()
|
|
86
|
+
|
|
87
|
+
logger.debug("starting directory artifact processing")
|
|
88
|
+
|
|
89
|
+
for artifact in dir_artifacts:
|
|
90
|
+
logger.info(
|
|
91
|
+
"processing artifact: [ FILE: %s | PATH: %s | FILE TYPE: %s ]",
|
|
92
|
+
artifact.name,
|
|
93
|
+
artifact.absolute_path,
|
|
94
|
+
artifact.mime_type,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if artifact.mime_type == "directory":
|
|
98
|
+
logger.info("subdirectory found, processing")
|
|
99
|
+
|
|
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")
|
|
106
|
+
|
|
107
|
+
video_metadata.import_metadata_from_file(
|
|
108
|
+
file_path=artifact.absolute_path
|
|
109
|
+
)
|
|
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}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
logger.debug("moving %s to %s", src_file, dst_file)
|
|
125
|
+
|
|
126
|
+
os.rename(src_file, dst_file)
|
|
127
|
+
else:
|
|
128
|
+
# delete the file
|
|
129
|
+
logger.info("unnecessary file found, deleting")
|
|
130
|
+
|
|
131
|
+
os.remove(artifact.absolute_path)
|
plexer/metadata.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plexer - Normalize media files for use with Plex Media Server
|
|
3
|
+
|
|
4
|
+
Module: Metadata - code for analyzing and managing video metadata
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from logzero import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Metadata:
|
|
12
|
+
"""
|
|
13
|
+
Code used for managing the metadata of a given video file, especially names
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
name = ""
|
|
17
|
+
release_year = 0
|
|
18
|
+
|
|
19
|
+
def __init__(self, name="", release_year=1900) -> None:
|
|
20
|
+
self.name = name
|
|
21
|
+
self.release_year = release_year
|
|
22
|
+
|
|
23
|
+
def import_metadata_from_file(self, file_path: str) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Read in given file and process data into metadata values
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
logger.debug("metadata file found @ %s - importing data", file_path)
|
|
29
|
+
|
|
30
|
+
with open(file_path, mode="r", encoding="utf-8") as metadata_file:
|
|
31
|
+
imported_metadata = json.load(metadata_file)
|
|
32
|
+
|
|
33
|
+
logger.debug("data imported as: %s", imported_metadata)
|
|
34
|
+
|
|
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]
|
|
41
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: plexer-cli
|
|
3
|
+
Version: 0.0.1a0
|
|
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
|
|
7
|
+
Project-URL: Issues, https://github.com/magneticstain/plexer/issues
|
|
8
|
+
Project-URL: Source, https://github.com/magneticstain/plexer
|
|
9
|
+
Author-email: Josh Carlson <837837+magneticstain@users.noreply.github.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: catagorization,formatting,media,organization,plex
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Framework :: Hatch
|
|
16
|
+
Classifier: Framework :: Pytest
|
|
17
|
+
Classifier: Framework :: tox
|
|
18
|
+
Classifier: Intended Audience :: System Administrators
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: Unix
|
|
21
|
+
Classifier: Programming Language :: Python
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: logzero~=1.7.0
|
|
29
|
+
Requires-Dist: python-magic~=0.4.27
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Plexer
|
|
33
|
+
|
|
34
|
+
Normalize media files for use with Plex Media Server.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
[](https://scorecard.dev/viewer/?uri=github.com/magneticstain/plexer)
|
|
40
|
+
[](https://app.codacy.com/gh/magneticstain/plexer/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
41
|
+
[](https://github.com/magneticstain/plexer/actions/workflows/run_full_test_suite.yml)
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
[](https://github.com/pypa/hatch)
|
|
47
|
+
[](https://github.com/astral-sh/ruff)
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
### Software Dependencies
|
|
52
|
+
|
|
53
|
+
Start by creating a virtual environment and installing the required packages. Typically that looks something like:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m venv .venv
|
|
57
|
+
source .venv/bin/activate
|
|
58
|
+
pip install -r requirements.txt
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Media Metadata
|
|
62
|
+
|
|
63
|
+
The biggest requirement before running plexer is to ensure that you've created a `.plexer` file in each of your target directories.
|
|
64
|
+
|
|
65
|
+
This is a JSON-formatted file that includes the movie metadata required by Plexer to perform its jobs.
|
|
66
|
+
|
|
67
|
+
#### Plexer File Generator
|
|
68
|
+
|
|
69
|
+
To easily create the `.plexer` file, you can use the one-liner below while in the movie's directory:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
echo -n "Media Name: ";read MEDIA_NAME;echo -n "Release Year (YYYY): ";read RELEASE_YEAR;echo "{\"name\": \"${MEDIA_NAME}\", \"release_year\": \"${RELEASE_YEAR}\"}" > .plexer
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
It can be modified to support different types of media as well.
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
The source directory is the directory containing the raw media. The destination is where you'd like to save the processed media to.
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
usage: plexer.py [-h] [-v] [--version] -s SOURCE_DIR -d DESTINATION_DIR
|
|
83
|
+
|
|
84
|
+
options:
|
|
85
|
+
-h, --help show this help message and exit
|
|
86
|
+
-v, --verbose Verbosity (-v, -vv, etc)
|
|
87
|
+
--version show program's version number and exit
|
|
88
|
+
-s SOURCE_DIR, --source-dir SOURCE_DIR
|
|
89
|
+
-d DESTINATION_DIR, --destination-dir DESTINATION_DIR
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
### Software Stack
|
|
95
|
+
|
|
96
|
+
For developing with Plexer, there are several tools that are in use:
|
|
97
|
+
|
|
98
|
+
1. Build Backend, Packaging, and Dependency Management:
|
|
99
|
+
1. [Hatch](https://hatch.pypa.io/1.12/)
|
|
100
|
+
1. Analysis Tools:
|
|
101
|
+
1. [Ruff](https://docs.astral.sh/ruff/)
|
|
102
|
+
1. [Codacy](https://app.codacy.com/gh/magneticstain/plexer/dashboard)
|
|
103
|
+
1. Testing:
|
|
104
|
+
1. [Pytest](https://docs.pytest.org/en/latest/)
|
|
105
|
+
1. [Tox](https://tox.wiki/en/stable/)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
plexer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
plexer/artifact.py,sha256=JnaWeEM9msrs_EZpfGn7Xr1TcG8A7mUG-7BXuKNwXdU,378
|
|
3
|
+
plexer/const.py,sha256=U49nn11PvkOcvUf5eg1_hsYs12ULg_f2VtL81hWje-w,178
|
|
4
|
+
plexer/file_manager.py,sha256=nfmBnQXK8JWPp4INfLE6kVqeD6NMxFnn1YoTmywI4Dg,4153
|
|
5
|
+
plexer/metadata.py,sha256=UQ0nazq3agv7-h4NiVHNlpf2z_6rPLCBQ3TfIXKn0F8,1170
|
|
6
|
+
plexer_cli-0.0.1a0.dist-info/METADATA,sha256=pBZ0Yz7v9GmKW90fqiXHR7kRTuynuQDDnqYZNQLXv6U,4562
|
|
7
|
+
plexer_cli-0.0.1a0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
8
|
+
plexer_cli-0.0.1a0.dist-info/licenses/LICENSE,sha256=cs3weOTez4n04m4foiWXOU6C6Pr83ORocewlbPDaJGs,1069
|
|
9
|
+
plexer_cli-0.0.1a0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Josh Carlson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|