plexutil 1.0.0__tar.gz
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.
- plexutil-1.0.0/LICENSE +21 -0
- plexutil-1.0.0/PKG-INFO +36 -0
- plexutil-1.0.0/README.md +2 -0
- plexutil-1.0.0/pyproject.toml +27 -0
- plexutil-1.0.0/setup.cfg +4 -0
- plexutil-1.0.0/src/__init__.py +0 -0
- plexutil-1.0.0/src/core/__init__.py +0 -0
- plexutil-1.0.0/src/core/library.py +224 -0
- plexutil-1.0.0/src/core/movie_library.py +68 -0
- plexutil-1.0.0/src/core/music_library.py +105 -0
- plexutil-1.0.0/src/core/playlist.py +145 -0
- plexutil-1.0.0/src/core/prompt.py +191 -0
- plexutil-1.0.0/src/core/tv_library.py +92 -0
- plexutil-1.0.0/src/dto/__init__.py +0 -0
- plexutil-1.0.0/src/dto/library_dto.py +25 -0
- plexutil-1.0.0/src/dto/library_preferences_dto.py +25 -0
- plexutil-1.0.0/src/dto/music_playlist_dto.py +22 -0
- plexutil-1.0.0/src/dto/music_playlist_file_dto.py +27 -0
- plexutil-1.0.0/src/dto/plex_config_dto.py +39 -0
- plexutil-1.0.0/src/dto/song_dto.py +18 -0
- plexutil-1.0.0/src/dto/tv_language_manifest_dto.py +22 -0
- plexutil-1.0.0/src/dto/tv_language_manifest_file_dto.py +23 -0
- plexutil-1.0.0/src/dto/user_instructions_dto.py +37 -0
- plexutil-1.0.0/src/enum/__init__.py +0 -0
- plexutil-1.0.0/src/enum/agent.py +7 -0
- plexutil-1.0.0/src/enum/file_type.py +35 -0
- plexutil-1.0.0/src/enum/language.py +31 -0
- plexutil-1.0.0/src/enum/library_name.py +7 -0
- plexutil-1.0.0/src/enum/library_type.py +7 -0
- plexutil-1.0.0/src/enum/scanner.py +7 -0
- plexutil-1.0.0/src/enum/user_request.py +50 -0
- plexutil-1.0.0/src/exception/__init__.py +0 -0
- plexutil-1.0.0/src/exception/library_op_error.py +28 -0
- plexutil-1.0.0/src/exception/library_poll_timeout_error.py +2 -0
- plexutil-1.0.0/src/exception/library_unsupported_error.py +13 -0
- plexutil-1.0.0/src/exception/plex_util_config_error.py +2 -0
- plexutil-1.0.0/src/plex_util_logger.py +54 -0
- plexutil-1.0.0/src/plexutil.egg-info/PKG-INFO +36 -0
- plexutil-1.0.0/src/plexutil.egg-info/SOURCES.txt +51 -0
- plexutil-1.0.0/src/plexutil.egg-info/dependency_links.txt +1 -0
- plexutil-1.0.0/src/plexutil.egg-info/top_level.txt +10 -0
- plexutil-1.0.0/src/serializer/__init__.py +0 -0
- plexutil-1.0.0/src/serializer/music_playlist_file_serializer.py +65 -0
- plexutil-1.0.0/src/serializer/plex_config_serializer.py +67 -0
- plexutil-1.0.0/src/serializer/serializable.py +3 -0
- plexutil-1.0.0/src/serializer/serializer.py +17 -0
- plexutil-1.0.0/src/serializer/tv_language_manifest_serializer.py +29 -0
- plexutil-1.0.0/src/static.py +7 -0
- plexutil-1.0.0/src/util/__init__.py +0 -0
- plexutil-1.0.0/src/util/file_importer.py +116 -0
- plexutil-1.0.0/src/util/path_ops.py +46 -0
- plexutil-1.0.0/src/util/plex_ops.py +16 -0
- plexutil-1.0.0/src/util/query_builder.py +67 -0
plexutil-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 CARLOS FLOREZ
|
|
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.
|
plexutil-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: plexutil
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Author-email: Carlos Florez <carlos@florez.co.uk>
|
|
5
|
+
Maintainer-email: Carlos Florez <carlos@florez.co.uk>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 CARLOS FLOREZ
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/florez-carlos/plexutil
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
31
|
+
Requires-Python: >=3.11.6
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
|
|
35
|
+
# plexutil
|
|
36
|
+
source init.sh
|
plexutil-1.0.0/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=62.6"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plexutil"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
requires-python = ">=3.11.6"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Carlos Florez", email = "carlos@florez.co.uk"}
|
|
11
|
+
]
|
|
12
|
+
maintainers = [
|
|
13
|
+
{name = "Carlos Florez", email = "carlos@florez.co.uk"}
|
|
14
|
+
]
|
|
15
|
+
description = ""
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
license = {file = "LICENSE"}
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.dynamic]
|
|
24
|
+
dependencies = {file = ["requirements.txt"]}
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Repository = "https://github.com/florez-carlos/plexutil"
|
plexutil-1.0.0/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from src.exception.library_poll_timeout_error import LibraryPollTimeoutError
|
|
8
|
+
from src.plex_util_logger import PlexUtilLogger
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from plexapi.audio import Audio
|
|
14
|
+
from plexapi.server import PlexServer
|
|
15
|
+
from plexapi.video import Video
|
|
16
|
+
|
|
17
|
+
from src.dto.library_preferences_dto import LibraryPreferencesDTO
|
|
18
|
+
from src.enum.agent import Agent
|
|
19
|
+
from src.enum.language import Language
|
|
20
|
+
from src.enum.library_name import LibraryName
|
|
21
|
+
from src.enum.scanner import Scanner
|
|
22
|
+
|
|
23
|
+
from alive_progress import alive_bar
|
|
24
|
+
from plexapi.exceptions import NotFound
|
|
25
|
+
|
|
26
|
+
from src.enum.library_type import LibraryType
|
|
27
|
+
from src.exception.library_op_error import LibraryOpError
|
|
28
|
+
from src.exception.library_unsupported_error import LibraryUnsupportedError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Library(ABC):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
plex_server: PlexServer,
|
|
35
|
+
name: LibraryName,
|
|
36
|
+
library_type: LibraryType,
|
|
37
|
+
agent: Agent,
|
|
38
|
+
scanner: Scanner,
|
|
39
|
+
location: Path,
|
|
40
|
+
language: Language,
|
|
41
|
+
preferences: LibraryPreferencesDTO,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.plex_server = plex_server
|
|
44
|
+
self.name = name
|
|
45
|
+
self.library_type = library_type
|
|
46
|
+
self.agent = agent
|
|
47
|
+
self.scanner = scanner
|
|
48
|
+
self.location = location
|
|
49
|
+
self.language = language
|
|
50
|
+
self.preferences = preferences
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def create(self) -> None:
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def delete(self) -> None:
|
|
58
|
+
op_type = "DELETE"
|
|
59
|
+
|
|
60
|
+
info = (
|
|
61
|
+
"Deleting library: \n"
|
|
62
|
+
f"Name: {self.name.value}\n"
|
|
63
|
+
f"Type: {self.library_type.value}\n"
|
|
64
|
+
f"Agent: {self.agent.value}\n"
|
|
65
|
+
f"Scanner: {self.scanner.value}\n"
|
|
66
|
+
f"Location: {self.location!s}\n"
|
|
67
|
+
f"Language: {self.language.value}\n"
|
|
68
|
+
f"Preferences: {self.preferences.music}\n"
|
|
69
|
+
)
|
|
70
|
+
PlexUtilLogger.get_logger().info(info)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
result = self.plex_server.library.section(self.name.value)
|
|
74
|
+
|
|
75
|
+
if result:
|
|
76
|
+
result.delete()
|
|
77
|
+
else:
|
|
78
|
+
description = "Nothing found"
|
|
79
|
+
raise LibraryOpError(
|
|
80
|
+
op_type=op_type,
|
|
81
|
+
description=description,
|
|
82
|
+
library_type=self.library_type,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
except NotFound:
|
|
86
|
+
raise LibraryOpError(
|
|
87
|
+
op_type=op_type,
|
|
88
|
+
library_type=self.library_type,
|
|
89
|
+
) from NotFound
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def exists(self) -> bool:
|
|
93
|
+
debug = (
|
|
94
|
+
"Checking library exists: \n"
|
|
95
|
+
f"Name: {self.name.value}\n"
|
|
96
|
+
f"Type: {self.library_type.value}\n"
|
|
97
|
+
f"Agent: {self.agent.value}\n"
|
|
98
|
+
f"Scanner: {self.scanner.value}\n"
|
|
99
|
+
f"Location: {self.location!s}\n"
|
|
100
|
+
f"Language: {self.language.value}\n"
|
|
101
|
+
f"Preferences: {self.preferences.movie}\n"
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
result = self.plex_server.library.section(self.name.value)
|
|
105
|
+
|
|
106
|
+
if not result:
|
|
107
|
+
debug = debug + "-Not found-"
|
|
108
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
except NotFound:
|
|
112
|
+
debug = debug + "-Not found-"
|
|
113
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def poll(
|
|
120
|
+
self,
|
|
121
|
+
requested_attempts: int = 0,
|
|
122
|
+
expected_count: int = 0,
|
|
123
|
+
interval_seconds: int = 0,
|
|
124
|
+
tvdb_ids: list[int] | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
current_count = len(self.query(tvdb_ids))
|
|
127
|
+
init_offset = abs(expected_count - current_count)
|
|
128
|
+
|
|
129
|
+
debug = (
|
|
130
|
+
f"Requested attempts: {requested_attempts!s}\n"
|
|
131
|
+
f"Interval seconds: {interval_seconds!s}\n"
|
|
132
|
+
f"Current count: {current_count!s}\n"
|
|
133
|
+
f"Expected count: {expected_count!s}\n"
|
|
134
|
+
f"Net change: {init_offset!s}\n"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
138
|
+
|
|
139
|
+
with alive_bar(init_offset) as bar:
|
|
140
|
+
attempts = 0
|
|
141
|
+
display_count = 0
|
|
142
|
+
offset = init_offset
|
|
143
|
+
|
|
144
|
+
while attempts < requested_attempts:
|
|
145
|
+
updated_current_count = len(self.query(tvdb_ids))
|
|
146
|
+
offset = abs(updated_current_count - current_count)
|
|
147
|
+
current_count = updated_current_count
|
|
148
|
+
|
|
149
|
+
if current_count == expected_count:
|
|
150
|
+
for _ in range(abs(current_count - display_count)):
|
|
151
|
+
bar()
|
|
152
|
+
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
for _ in range(offset):
|
|
156
|
+
display_count = display_count + 1
|
|
157
|
+
bar()
|
|
158
|
+
|
|
159
|
+
time.sleep(interval_seconds)
|
|
160
|
+
attempts = attempts + 1
|
|
161
|
+
if attempts >= requested_attempts:
|
|
162
|
+
raise LibraryPollTimeoutError
|
|
163
|
+
|
|
164
|
+
def query(
|
|
165
|
+
self,
|
|
166
|
+
tvdb_ids: list[int] | None = None,
|
|
167
|
+
) -> list[Audio] | list[Video]:
|
|
168
|
+
op_type = "QUERY"
|
|
169
|
+
|
|
170
|
+
if tvdb_ids is None:
|
|
171
|
+
tvdb_ids = []
|
|
172
|
+
|
|
173
|
+
debug = (
|
|
174
|
+
"Performing query:\n"
|
|
175
|
+
f"Name: {self.name.value}\n"
|
|
176
|
+
f"Library Type: {self.library_type.value}\n"
|
|
177
|
+
f"TVDB Ids: {tvdb_ids}\n"
|
|
178
|
+
)
|
|
179
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
if self.library_type is LibraryType.MUSIC:
|
|
183
|
+
return self.plex_server.library.section(
|
|
184
|
+
self.name.value,
|
|
185
|
+
).searchTracks()
|
|
186
|
+
|
|
187
|
+
elif self.library_type is LibraryType.TV:
|
|
188
|
+
shows = self.plex_server.library.section(self.name.value).all()
|
|
189
|
+
shows_filtered = []
|
|
190
|
+
|
|
191
|
+
if tvdb_ids:
|
|
192
|
+
for show in shows:
|
|
193
|
+
guids = show.guids
|
|
194
|
+
tvdb_prefix = "tvdb://"
|
|
195
|
+
for guid in guids:
|
|
196
|
+
if tvdb_prefix in guid.id:
|
|
197
|
+
tvdb = guid.id.replace(tvdb_prefix, "")
|
|
198
|
+
if int(tvdb) in tvdb_ids:
|
|
199
|
+
shows_filtered.append(show)
|
|
200
|
+
else:
|
|
201
|
+
description = (
|
|
202
|
+
"Expected ("
|
|
203
|
+
+ tvdb_prefix
|
|
204
|
+
+ ") but show does not have any: "
|
|
205
|
+
+ guid.id
|
|
206
|
+
)
|
|
207
|
+
LibraryOpError(
|
|
208
|
+
op_type=op_type,
|
|
209
|
+
library_type=self.library_type,
|
|
210
|
+
description=description,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return shows_filtered
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
raise LibraryUnsupportedError(
|
|
217
|
+
op_type=op_type,
|
|
218
|
+
library_type=self.library_type,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
except NotFound:
|
|
222
|
+
debug = "Received Not Found on a Query operation"
|
|
223
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
224
|
+
return []
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from plexapi.server import PlexServer
|
|
4
|
+
|
|
5
|
+
from src.core.library import Library
|
|
6
|
+
from src.dto.library_preferences_dto import LibraryPreferencesDTO
|
|
7
|
+
from src.enum.agent import Agent
|
|
8
|
+
from src.enum.language import Language
|
|
9
|
+
from src.enum.library_name import LibraryName
|
|
10
|
+
from src.enum.library_type import LibraryType
|
|
11
|
+
from src.enum.scanner import Scanner
|
|
12
|
+
from src.plex_util_logger import PlexUtilLogger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MovieLibrary(Library):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
plex_server: PlexServer,
|
|
19
|
+
location: Path,
|
|
20
|
+
language: Language,
|
|
21
|
+
preferences: LibraryPreferencesDTO,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(
|
|
24
|
+
plex_server,
|
|
25
|
+
LibraryName.MOVIE,
|
|
26
|
+
LibraryType.MOVIE,
|
|
27
|
+
Agent.MOVIE,
|
|
28
|
+
Scanner.MOVIE,
|
|
29
|
+
location,
|
|
30
|
+
language,
|
|
31
|
+
preferences,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def create(self) -> None:
|
|
35
|
+
info = (
|
|
36
|
+
"Creating movie library: \n"
|
|
37
|
+
f"Name: {self.name.value}\n"
|
|
38
|
+
f"Type: {self.library_type.value}\n"
|
|
39
|
+
f"Agent: {self.agent.value}\n"
|
|
40
|
+
f"Scanner: {self.scanner.value}\n"
|
|
41
|
+
f"Location: {self.location!s}\n"
|
|
42
|
+
f"Language: {self.language.value}\n"
|
|
43
|
+
f"Preferences: {self.preferences.movie}\n"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
PlexUtilLogger.get_logger().info(info)
|
|
47
|
+
|
|
48
|
+
self.plex_server.library.add(
|
|
49
|
+
name=self.name.value,
|
|
50
|
+
type=self.library_type.value,
|
|
51
|
+
agent=self.agent.value,
|
|
52
|
+
scanner=self.scanner.value,
|
|
53
|
+
location=str(self.location),
|
|
54
|
+
language=self.language.value,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# This line triggers a refresh of the library
|
|
58
|
+
self.plex_server.library.sections()
|
|
59
|
+
|
|
60
|
+
self.plex_server.library.section(self.name.value).editAdvanced(
|
|
61
|
+
**self.preferences.movie,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def delete(self) -> None:
|
|
65
|
+
return super().delete()
|
|
66
|
+
|
|
67
|
+
def exists(self) -> bool:
|
|
68
|
+
return super().exists()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from plexapi.server import PlexServer
|
|
4
|
+
|
|
5
|
+
from src.core.library import Library
|
|
6
|
+
from src.dto.library_preferences_dto import LibraryPreferencesDTO
|
|
7
|
+
from src.dto.music_playlist_file_dto import MusicPlaylistFileDTO
|
|
8
|
+
from src.enum.agent import Agent
|
|
9
|
+
from src.enum.language import Language
|
|
10
|
+
from src.enum.library_name import LibraryName
|
|
11
|
+
from src.enum.library_type import LibraryType
|
|
12
|
+
from src.enum.scanner import Scanner
|
|
13
|
+
from src.exception.library_op_error import LibraryOpError
|
|
14
|
+
from src.plex_util_logger import PlexUtilLogger
|
|
15
|
+
from src.util.query_builder import QueryBuilder
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MusicLibrary(Library):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
plex_server: PlexServer,
|
|
22
|
+
location: Path,
|
|
23
|
+
language: Language,
|
|
24
|
+
preferences: LibraryPreferencesDTO,
|
|
25
|
+
music_playlist_file_dto: MusicPlaylistFileDTO,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(
|
|
28
|
+
plex_server,
|
|
29
|
+
LibraryName.MUSIC,
|
|
30
|
+
LibraryType.MUSIC,
|
|
31
|
+
Agent.MUSIC,
|
|
32
|
+
Scanner.MUSIC,
|
|
33
|
+
location,
|
|
34
|
+
language,
|
|
35
|
+
preferences,
|
|
36
|
+
)
|
|
37
|
+
self.music_playlist_file_dto = music_playlist_file_dto
|
|
38
|
+
|
|
39
|
+
def create(self) -> None:
|
|
40
|
+
op_type = "CREATE"
|
|
41
|
+
|
|
42
|
+
part = ""
|
|
43
|
+
|
|
44
|
+
query_builder = QueryBuilder(
|
|
45
|
+
"/library/sections",
|
|
46
|
+
name=LibraryName.MUSIC.value,
|
|
47
|
+
the_type=LibraryType.MUSIC.value,
|
|
48
|
+
agent=Agent.MUSIC.value,
|
|
49
|
+
scanner=Scanner.MUSIC.value,
|
|
50
|
+
language=Language.ENGLISH_US.value,
|
|
51
|
+
importFromiTunes="",
|
|
52
|
+
enableAutoPhotoTags="",
|
|
53
|
+
location=str(self.location),
|
|
54
|
+
prefs=self.preferences.music,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
part = query_builder.build()
|
|
58
|
+
|
|
59
|
+
info = (
|
|
60
|
+
"Creating music library: \n"
|
|
61
|
+
f"Name: {self.name.value}\n"
|
|
62
|
+
f"Type: {self.library_type.value}\n"
|
|
63
|
+
f"Agent: {self.agent.value}\n"
|
|
64
|
+
f"Scanner: {self.scanner.value}\n"
|
|
65
|
+
f"Location: {self.location!s}\n"
|
|
66
|
+
f"Language: {self.language.value}\n"
|
|
67
|
+
f"Preferences: {self.preferences.music}\n"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
debug = f"Query: {part}\n"
|
|
71
|
+
|
|
72
|
+
PlexUtilLogger.get_logger().info(info)
|
|
73
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
74
|
+
|
|
75
|
+
# This posts a music library
|
|
76
|
+
if part:
|
|
77
|
+
self.plex_server.query(
|
|
78
|
+
part,
|
|
79
|
+
method=self.plex_server._session.post,
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
description = "Query Builder has not built a part!"
|
|
83
|
+
raise LibraryOpError(
|
|
84
|
+
op_type=op_type,
|
|
85
|
+
library_type=self.library_type,
|
|
86
|
+
description=description,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# This triggers a refresh of the library
|
|
90
|
+
self.plex_server.library.sections()
|
|
91
|
+
|
|
92
|
+
info = (
|
|
93
|
+
"Checking server music "
|
|
94
|
+
"meets expected "
|
|
95
|
+
f"count: {self.music_playlist_file_dto.track_count!s}\n"
|
|
96
|
+
)
|
|
97
|
+
PlexUtilLogger.get_logger().info(info)
|
|
98
|
+
|
|
99
|
+
self.poll(200, self.music_playlist_file_dto.track_count, 10)
|
|
100
|
+
|
|
101
|
+
def delete(self) -> None:
|
|
102
|
+
return super().delete()
|
|
103
|
+
|
|
104
|
+
def exists(self) -> bool:
|
|
105
|
+
return super().exists()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from plexapi.server import PlexServer
|
|
4
|
+
|
|
5
|
+
from src.core.library import Library
|
|
6
|
+
from src.dto.library_preferences_dto import LibraryPreferencesDTO
|
|
7
|
+
from src.dto.music_playlist_file_dto import MusicPlaylistFileDTO
|
|
8
|
+
from src.enum.agent import Agent
|
|
9
|
+
from src.enum.language import Language
|
|
10
|
+
from src.enum.library_name import LibraryName
|
|
11
|
+
from src.enum.library_type import LibraryType
|
|
12
|
+
from src.enum.scanner import Scanner
|
|
13
|
+
from src.exception.library_op_error import LibraryOpError
|
|
14
|
+
from src.plex_util_logger import PlexUtilLogger
|
|
15
|
+
from src.util.path_ops import PathOps
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Playlist(Library):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
plex_server: PlexServer,
|
|
22
|
+
location: Path,
|
|
23
|
+
language: Language,
|
|
24
|
+
music_playlist_file_dto: MusicPlaylistFileDTO,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(
|
|
27
|
+
plex_server,
|
|
28
|
+
LibraryName.MUSIC,
|
|
29
|
+
LibraryType.MUSIC,
|
|
30
|
+
Agent.MUSIC,
|
|
31
|
+
Scanner.MUSIC,
|
|
32
|
+
location,
|
|
33
|
+
language,
|
|
34
|
+
LibraryPreferencesDTO({}, {}, {}, {}),
|
|
35
|
+
)
|
|
36
|
+
self.music_playlist_file_dto = music_playlist_file_dto
|
|
37
|
+
|
|
38
|
+
def create(self) -> None:
|
|
39
|
+
op_type = "CREATE"
|
|
40
|
+
tracks = self.plex_server.library.section(
|
|
41
|
+
self.name.value,
|
|
42
|
+
).searchTracks()
|
|
43
|
+
plex_track_dict = {}
|
|
44
|
+
plex_playlist = []
|
|
45
|
+
|
|
46
|
+
playlist_names = [
|
|
47
|
+
x.name for x in self.music_playlist_file_dto.playlists
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
info = "Creating playlist library: \n" f"Playlists: {playlist_names}\n"
|
|
51
|
+
|
|
52
|
+
PlexUtilLogger.get_logger().info(info)
|
|
53
|
+
|
|
54
|
+
info = (
|
|
55
|
+
"Checking server track count "
|
|
56
|
+
f"meets expected "
|
|
57
|
+
f"count: {self.music_playlist_file_dto.track_count!s}\n"
|
|
58
|
+
)
|
|
59
|
+
PlexUtilLogger.get_logger().info(info)
|
|
60
|
+
self.poll(10, self.music_playlist_file_dto.track_count, 10)
|
|
61
|
+
|
|
62
|
+
playlists = self.music_playlist_file_dto.playlists
|
|
63
|
+
|
|
64
|
+
for track in tracks:
|
|
65
|
+
plex_track_absolute_location = track.locations[0]
|
|
66
|
+
plex_track_path = PathOps.get_path_from_str(
|
|
67
|
+
plex_track_absolute_location,
|
|
68
|
+
)
|
|
69
|
+
plex_track_full_name = plex_track_path.name
|
|
70
|
+
plex_track_name = plex_track_full_name.rsplit(".", 1)[0]
|
|
71
|
+
plex_track_dict[plex_track_name] = track
|
|
72
|
+
|
|
73
|
+
for playlist in playlists:
|
|
74
|
+
playlist_name = playlist.name
|
|
75
|
+
songs = playlist.songs
|
|
76
|
+
|
|
77
|
+
for song in songs:
|
|
78
|
+
song_name = song.name
|
|
79
|
+
|
|
80
|
+
if plex_track_dict.get(song_name) is None:
|
|
81
|
+
description = (
|
|
82
|
+
f"File in music playlist: '{song_name}' "
|
|
83
|
+
"does not exist in server"
|
|
84
|
+
)
|
|
85
|
+
raise LibraryOpError(
|
|
86
|
+
op_type=op_type,
|
|
87
|
+
library_type=self.library_type,
|
|
88
|
+
description=description,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
plex_playlist.append(plex_track_dict.get(song_name))
|
|
92
|
+
|
|
93
|
+
self.plex_server.createPlaylist(
|
|
94
|
+
title=playlist_name,
|
|
95
|
+
items=plex_playlist,
|
|
96
|
+
)
|
|
97
|
+
plex_playlist = []
|
|
98
|
+
|
|
99
|
+
def delete(self) -> None:
|
|
100
|
+
playlist_names = [
|
|
101
|
+
x.name for x in self.music_playlist_file_dto.playlists
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
info = (
|
|
105
|
+
"Deleting music playlists: \n"
|
|
106
|
+
f"Playlists: {playlist_names}\n"
|
|
107
|
+
f"Location: {self.location!s}\n"
|
|
108
|
+
)
|
|
109
|
+
PlexUtilLogger.get_logger().info(info)
|
|
110
|
+
|
|
111
|
+
server_playlists = self.plex_server.playlists(playlistType="audio")
|
|
112
|
+
|
|
113
|
+
debug = f"Playlists available in server: {server_playlists}"
|
|
114
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
115
|
+
|
|
116
|
+
for playlist in server_playlists:
|
|
117
|
+
if playlist.title in playlist_names:
|
|
118
|
+
playlist.delete()
|
|
119
|
+
|
|
120
|
+
def exists(self) -> bool:
|
|
121
|
+
playlist_names = [
|
|
122
|
+
x.name for x in self.music_playlist_file_dto.playlists
|
|
123
|
+
]
|
|
124
|
+
playlists = self.plex_server.playlists(playlistType="audio")
|
|
125
|
+
|
|
126
|
+
debug = (
|
|
127
|
+
f"Checking playlists exist\n"
|
|
128
|
+
f"Requested: {playlist_names}\n"
|
|
129
|
+
f"In server: {playlists}\n"
|
|
130
|
+
)
|
|
131
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
132
|
+
|
|
133
|
+
if not playlists or not playlist_names:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
all_exist = True
|
|
137
|
+
for playlist_name in playlist_names:
|
|
138
|
+
if playlist_name in [x.title for x in playlists]:
|
|
139
|
+
continue
|
|
140
|
+
all_exist = False
|
|
141
|
+
|
|
142
|
+
debug = f"All exist: {all_exist}"
|
|
143
|
+
PlexUtilLogger.get_logger().debug(debug)
|
|
144
|
+
|
|
145
|
+
return all_exist
|