plexutil 3.1.4__tar.gz → 3.2.1__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-3.1.4 → plexutil-3.2.1}/PKG-INFO +4 -8
- {plexutil-3.1.4 → plexutil-3.2.1}/README.md +2 -7
- {plexutil-3.1.4 → plexutil-3.2.1}/pyproject.toml +2 -1
- {plexutil-3.1.4 → plexutil-3.2.1}/requirements.txt +1 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/__main__.py +10 -3
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/library.py +46 -22
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/movie_library.py +23 -2
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/music_library.py +5 -2
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/music_playlist.py +33 -6
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/prompt.py +61 -3
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/tv_library.py +22 -2
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/library_setting.py +4 -7
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/user_request.py +0 -2
- plexutil-3.2.1/src/plexutil/exception/server_connection_error.py +2 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/icons.py +4 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/PKG-INFO +4 -8
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/SOURCES.txt +1 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/requires.txt +1 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/.github/workflows/python-publish.yml +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/.gitignore +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/LICENSE +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/MANIFEST.in +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/git-hooks/commit-msg +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/git-hooks/pre-commit +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/init.sh +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/ruff.toml +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/setup.cfg +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/config/log_config.yaml +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/auth.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/library_factory.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/bootstrap_paths_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/dropdown_item_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/library_setting_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/movie_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/music_playlist_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/song_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/tv_episode_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/tv_series_dto.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/agent.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/file_type.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/language.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/library_type.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/scanner.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/server_setting.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/auth_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/bootstrap_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/database_connection_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/device_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/entity_not_found_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_illegal_state_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_op_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_poll_timeout_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_section_missing_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_unsupported_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/plex_media_missing_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/unexpected_argument_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/unexpected_naming_pattern_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/user_error.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/graphical/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/graphical/selection_window.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/mapper/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/mapper/music_playlist_mapper.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/mapper/song_mapper.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/music_playlist_entity.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/song_entity.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/song_music_playlist_entity.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/plex_util_logger.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/db_manager.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/music_playlist_service.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/song_music_playlist_composite_service.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/song_service.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/static.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/__init__.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/file_importer.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/plex_ops.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/query_builder.py +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/dependency_links.txt +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/entry_points.txt +0 -0
- {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexutil
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Author-email: Carlos Florez <carlos@florez.co.uk>
|
|
5
5
|
Maintainer-email: Carlos Florez <carlos@florez.co.uk>
|
|
6
6
|
License: MIT License
|
|
@@ -42,6 +42,7 @@ Requires-Dist: peewee==3.18.3
|
|
|
42
42
|
Requires-Dist: colorlog==6.10.1
|
|
43
43
|
Requires-Dist: pyjwt[crypto]==2.10.1
|
|
44
44
|
Requires-Dist: ttkthemes==3.3.0
|
|
45
|
+
Requires-Dist: yaspin==3.4.0
|
|
45
46
|
Dynamic: license-file
|
|
46
47
|
|
|
47
48
|
# Plexutil
|
|
@@ -110,15 +111,10 @@ plexutil upload
|
|
|
110
111
|
> [!WARNING]
|
|
111
112
|
> This feature requires a graphical session (X11 or Wayland) <br>
|
|
112
113
|
|
|
113
|
-
To add songs to an existing music playlist
|
|
114
|
+
To add songs to an existing music playlist or to remove songs from an existing music playlist
|
|
114
115
|
```bash
|
|
115
|
-
plexutil
|
|
116
|
+
plexutil modify
|
|
116
117
|
```
|
|
117
|
-
To remove songs from an existing music playlist
|
|
118
|
-
```bash
|
|
119
|
-
plexutil remove_from_playlist
|
|
120
|
-
```
|
|
121
|
-
|
|
122
118
|
|
|
123
119
|
## Development
|
|
124
120
|
> [!NOTE]
|
|
@@ -64,15 +64,10 @@ plexutil upload
|
|
|
64
64
|
> [!WARNING]
|
|
65
65
|
> This feature requires a graphical session (X11 or Wayland) <br>
|
|
66
66
|
|
|
67
|
-
To add songs to an existing music playlist
|
|
67
|
+
To add songs to an existing music playlist or to remove songs from an existing music playlist
|
|
68
68
|
```bash
|
|
69
|
-
plexutil
|
|
69
|
+
plexutil modify
|
|
70
70
|
```
|
|
71
|
-
To remove songs from an existing music playlist
|
|
72
|
-
```bash
|
|
73
|
-
plexutil remove_from_playlist
|
|
74
|
-
```
|
|
75
|
-
|
|
76
71
|
|
|
77
72
|
## Development
|
|
78
73
|
> [!NOTE]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "plexutil"
|
|
7
|
-
version = "3.1
|
|
7
|
+
version = "3.2.1"
|
|
8
8
|
requires-python = ">=3.11"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Carlos Florez", email = "carlos@florez.co.uk"}
|
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
|
31
31
|
"colorlog==6.10.1",
|
|
32
32
|
"pyjwt[crypto]==2.10.1",
|
|
33
33
|
"ttkthemes==3.3.0",
|
|
34
|
+
"yaspin==3.4.0",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.scripts]
|
|
@@ -18,6 +18,7 @@ from plexutil.exception.library_poll_timeout_error import (
|
|
|
18
18
|
from plexutil.exception.library_section_missing_error import (
|
|
19
19
|
LibrarySectionMissingError,
|
|
20
20
|
)
|
|
21
|
+
from plexutil.exception.server_connection_error import ServerConnectionError
|
|
21
22
|
from plexutil.exception.unexpected_argument_error import (
|
|
22
23
|
UnexpectedArgumentError,
|
|
23
24
|
)
|
|
@@ -46,9 +47,7 @@ def main() -> None:
|
|
|
46
47
|
bootstrap_paths_dto.token_dir.unlink(missing_ok=True)
|
|
47
48
|
plex_resources = Auth.get_resources(bootstrap_paths_dto)
|
|
48
49
|
|
|
49
|
-
plex_server = Prompt.confirm_server(
|
|
50
|
-
plex_resources=plex_resources
|
|
51
|
-
).connect()
|
|
50
|
+
plex_server = Prompt.confirm_server(plex_resources=plex_resources)
|
|
52
51
|
|
|
53
52
|
if user_request is UserRequest.SETTINGS:
|
|
54
53
|
PlexOps.set_server_settings(plex_server=plex_server)
|
|
@@ -80,6 +79,14 @@ def main() -> None:
|
|
|
80
79
|
PlexUtilLogger.get_logger().error(description)
|
|
81
80
|
sys.exit(1)
|
|
82
81
|
|
|
82
|
+
except ServerConnectionError as e:
|
|
83
|
+
sys.tracebacklimit = 0
|
|
84
|
+
description = (
|
|
85
|
+
f"\n{Icons.BANNER_LEFT}Connection Error{Icons.BANNER_RIGHT}\n{e!s}"
|
|
86
|
+
)
|
|
87
|
+
PlexUtilLogger.get_logger().error(description)
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
83
90
|
except LibraryIllegalStateError as e:
|
|
84
91
|
sys.tracebacklimit = 0
|
|
85
92
|
description = (
|
|
@@ -87,12 +87,6 @@ class Library(ABC):
|
|
|
87
87
|
self.display(expect_input=True)
|
|
88
88
|
self.modify()
|
|
89
89
|
self.update()
|
|
90
|
-
case UserRequest.ADD_TO_PLAYLIST:
|
|
91
|
-
self.display(expect_input=True)
|
|
92
|
-
self.add_item()
|
|
93
|
-
case UserRequest.REMOVE_FROM_PLAYLIST:
|
|
94
|
-
self.display(expect_input=True)
|
|
95
|
-
self.remove_item()
|
|
96
90
|
|
|
97
91
|
@abstractmethod
|
|
98
92
|
def download(self) -> None:
|
|
@@ -110,27 +104,61 @@ class Library(ABC):
|
|
|
110
104
|
def remove_item(self) -> None:
|
|
111
105
|
raise NotImplementedError
|
|
112
106
|
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def query(self) -> list[Track] | list[Show] | list[Movie] | list[Playlist]:
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def display_media(
|
|
113
|
+
self, expect_input: bool = False
|
|
114
|
+
) -> Movie | Show | Track:
|
|
115
|
+
raise NotImplementedError
|
|
116
|
+
|
|
113
117
|
@abstractmethod
|
|
114
118
|
def update(self) -> None:
|
|
115
119
|
section = self.get_section()
|
|
116
120
|
section.update()
|
|
117
121
|
section.refresh()
|
|
122
|
+
[media.refresh() for media in self.query()]
|
|
118
123
|
|
|
119
124
|
@abstractmethod
|
|
120
|
-
def modify(self) -> None:
|
|
121
|
-
settings = LibrarySetting.get_all(self.library_type)
|
|
125
|
+
def modify(self, is_modify_media: bool = False) -> None:
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
agent=self.agent.get_value(),
|
|
126
|
-
scanner=self.scanner.get_value(),
|
|
127
|
-
language=self.language.get_value(),
|
|
128
|
-
)
|
|
127
|
+
if is_modify_media:
|
|
128
|
+
selected_media = self.display_media(expect_input=True)
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
language = self.language
|
|
131
|
+
language_override = selected_media.languageOverride
|
|
132
|
+
if language_override:
|
|
133
|
+
language = Language.get_from_str(language_override)
|
|
134
|
+
|
|
135
|
+
selected_language = Prompt.confirm_language(
|
|
136
|
+
default=language,
|
|
137
|
+
is_from_server=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if selected_language is self.language:
|
|
141
|
+
# Sets to Libary Default if selected language matches Library
|
|
142
|
+
selected_media.editAdvanced(languageOverride="") # pyright: ignore [reportOptionalCall]
|
|
143
|
+
else:
|
|
144
|
+
selected_media.editAdvanced(
|
|
145
|
+
languageOverride=selected_language.get_value() # pyright: ignore [reportOptionalCall]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
settings = LibrarySetting.get_all(self.library_type)
|
|
150
|
+
|
|
151
|
+
self.assign_language(default=self.language, is_from_server=True)
|
|
152
|
+
self.get_section().edit(
|
|
153
|
+
agent=self.agent.get_value(),
|
|
154
|
+
scanner=self.scanner.get_value(),
|
|
155
|
+
language=self.language.get_value(),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
PlexOps.set_library_settings(
|
|
159
|
+
section=self.get_section(),
|
|
160
|
+
settings=[x.to_dto(is_from_server=True) for x in settings],
|
|
161
|
+
)
|
|
134
162
|
|
|
135
163
|
@abstractmethod
|
|
136
164
|
def create(self) -> None:
|
|
@@ -436,10 +464,6 @@ class Library(ABC):
|
|
|
436
464
|
|
|
437
465
|
PlexUtilLogger.get_logger().debug(debug)
|
|
438
466
|
|
|
439
|
-
@abstractmethod
|
|
440
|
-
def query(self) -> list[Track] | list[Show] | list[Movie] | list[Playlist]:
|
|
441
|
-
raise NotImplementedError
|
|
442
|
-
|
|
443
467
|
def log_library(
|
|
444
468
|
self,
|
|
445
469
|
operation: str,
|
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import field
|
|
4
4
|
from typing import TYPE_CHECKING, cast
|
|
5
5
|
|
|
6
|
+
from plexutil.core.prompt import Prompt
|
|
7
|
+
from plexutil.dto.dropdown_item_dto import DropdownItemDTO
|
|
8
|
+
|
|
6
9
|
if TYPE_CHECKING:
|
|
7
10
|
from pathlib import Path
|
|
8
11
|
|
|
@@ -65,12 +68,30 @@ class MovieLibrary(Library):
|
|
|
65
68
|
def update(self) -> None:
|
|
66
69
|
super().update()
|
|
67
70
|
|
|
68
|
-
def modify(self) -> None:
|
|
69
|
-
|
|
71
|
+
def modify(self, is_modify_media: bool = False) -> None:
|
|
72
|
+
is_modify_media = Prompt.confirm_media_modification()
|
|
73
|
+
super().modify(is_modify_media=is_modify_media)
|
|
70
74
|
|
|
71
75
|
def display(self, expect_input: bool = False) -> None:
|
|
72
76
|
super().display(expect_input=expect_input)
|
|
73
77
|
|
|
78
|
+
def display_media(self, expect_input: bool = False) -> Movie:
|
|
79
|
+
title = "Choose Movie to modify"
|
|
80
|
+
description = ""
|
|
81
|
+
dropdown = [
|
|
82
|
+
DropdownItemDTO(
|
|
83
|
+
display_name=movie.title, value=movie, is_default=False
|
|
84
|
+
)
|
|
85
|
+
for movie in self.query()
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
return Prompt.draw_dropdown(
|
|
89
|
+
title=title,
|
|
90
|
+
description=description,
|
|
91
|
+
dropdown=dropdown,
|
|
92
|
+
expect_input=expect_input,
|
|
93
|
+
).value
|
|
94
|
+
|
|
74
95
|
def create(self) -> None:
|
|
75
96
|
super().create()
|
|
76
97
|
|
|
@@ -68,11 +68,14 @@ class MusicLibrary(Library):
|
|
|
68
68
|
def remove_item(self) -> None:
|
|
69
69
|
raise NotImplementedError
|
|
70
70
|
|
|
71
|
+
def display_media(self, expect_input: bool = False) -> Track:
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
71
74
|
def update(self) -> None:
|
|
72
75
|
super().update()
|
|
73
76
|
|
|
74
|
-
def modify(self) -> None:
|
|
75
|
-
super().modify()
|
|
77
|
+
def modify(self, is_modify_media: bool = False) -> None:
|
|
78
|
+
super().modify(is_modify_media=is_modify_media)
|
|
76
79
|
|
|
77
80
|
def display(self, expect_input: bool = False) -> None:
|
|
78
81
|
super().display(expect_input=expect_input)
|
|
@@ -51,8 +51,7 @@ class MusicPlaylist(Library):
|
|
|
51
51
|
UserRequest.DISPLAY,
|
|
52
52
|
UserRequest.UPLOAD,
|
|
53
53
|
UserRequest.DOWNLOAD,
|
|
54
|
-
UserRequest.
|
|
55
|
-
UserRequest.REMOVE_FROM_PLAYLIST,
|
|
54
|
+
UserRequest.MODIFY,
|
|
56
55
|
],
|
|
57
56
|
plex_server=plex_server,
|
|
58
57
|
name=name,
|
|
@@ -121,11 +120,39 @@ class MusicPlaylist(Library):
|
|
|
121
120
|
raise NotImplementedError
|
|
122
121
|
|
|
123
122
|
def update(self) -> None:
|
|
124
|
-
|
|
123
|
+
# None of MusicPlaylist operations benefit from a refresh/reload
|
|
124
|
+
return
|
|
125
125
|
|
|
126
|
-
def
|
|
126
|
+
def display_media(self, expect_input: bool = False) -> Track:
|
|
127
127
|
raise NotImplementedError
|
|
128
128
|
|
|
129
|
+
def modify(self, is_modify_media: bool = False) -> None: # noqa: ARG002
|
|
130
|
+
|
|
131
|
+
action_add_display_name = "Add Songs To Playlist"
|
|
132
|
+
action_delete_display_name = "Delete Songs From Playlist"
|
|
133
|
+
|
|
134
|
+
actions = [
|
|
135
|
+
DropdownItemDTO(
|
|
136
|
+
display_name=action_add_display_name,
|
|
137
|
+
value=False,
|
|
138
|
+
is_default=True,
|
|
139
|
+
),
|
|
140
|
+
DropdownItemDTO(
|
|
141
|
+
display_name=action_delete_display_name,
|
|
142
|
+
value=False,
|
|
143
|
+
is_default=False,
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
title = "Playlist Modification"
|
|
147
|
+
description = "Select from the supported modifications"
|
|
148
|
+
selected_action = Prompt.draw_dropdown(
|
|
149
|
+
title=title, description=description, dropdown=actions
|
|
150
|
+
)
|
|
151
|
+
if selected_action.display_name == action_add_display_name:
|
|
152
|
+
self.add_item()
|
|
153
|
+
elif selected_action.display_name == action_delete_display_name:
|
|
154
|
+
self.remove_item()
|
|
155
|
+
|
|
129
156
|
def display(self, expect_input: bool = False) -> None: # noqa: ARG002
|
|
130
157
|
super().display(expect_input=True)
|
|
131
158
|
if (
|
|
@@ -150,8 +177,8 @@ class MusicPlaylist(Library):
|
|
|
150
177
|
self.playlist_name = selected_playlist.title
|
|
151
178
|
|
|
152
179
|
if (
|
|
153
|
-
self.user_request is UserRequest.
|
|
154
|
-
or self.user_request is UserRequest.
|
|
180
|
+
self.user_request is UserRequest.MODIFY
|
|
181
|
+
or self.user_request is UserRequest.DELETE
|
|
155
182
|
):
|
|
156
183
|
return
|
|
157
184
|
|
|
@@ -8,7 +8,11 @@ from argparse import RawTextHelpFormatter
|
|
|
8
8
|
from importlib.metadata import PackageNotFoundError, version
|
|
9
9
|
from typing import TYPE_CHECKING, cast
|
|
10
10
|
|
|
11
|
+
from plexapi.exceptions import NotFound
|
|
12
|
+
from yaspin import yaspin
|
|
13
|
+
|
|
11
14
|
from plexutil.exception.device_error import DeviceError
|
|
15
|
+
from plexutil.exception.server_connection_error import ServerConnectionError
|
|
12
16
|
from plexutil.graphical.selection_window import SelectionWindow
|
|
13
17
|
|
|
14
18
|
if TYPE_CHECKING:
|
|
@@ -20,6 +24,7 @@ if TYPE_CHECKING:
|
|
|
20
24
|
ShowSection,
|
|
21
25
|
)
|
|
22
26
|
from plexapi.myplex import MyPlexResource
|
|
27
|
+
from plexapi.server import PlexServer
|
|
23
28
|
from plexapi.video import Movie, Show
|
|
24
29
|
|
|
25
30
|
from plexutil.core.library import Library
|
|
@@ -383,7 +388,7 @@ class Prompt(Static):
|
|
|
383
388
|
).value
|
|
384
389
|
|
|
385
390
|
@staticmethod
|
|
386
|
-
def confirm_server(plex_resources: list[MyPlexResource]) ->
|
|
391
|
+
def confirm_server(plex_resources: list[MyPlexResource]) -> PlexServer:
|
|
387
392
|
"""
|
|
388
393
|
Prompts user for a Plex Media Server selection
|
|
389
394
|
|
|
@@ -392,7 +397,9 @@ class Prompt(Static):
|
|
|
392
397
|
anything other than a Plex Media Server is filtered
|
|
393
398
|
|
|
394
399
|
Returns:
|
|
395
|
-
|
|
400
|
+
PlexServer: The chosen Plex Media Server
|
|
401
|
+
Raises:
|
|
402
|
+
ServerConnectionError: If unable to connect
|
|
396
403
|
"""
|
|
397
404
|
is_default = True
|
|
398
405
|
dropdown = []
|
|
@@ -406,12 +413,38 @@ class Prompt(Static):
|
|
|
406
413
|
is_default = False
|
|
407
414
|
dropdown.append(item)
|
|
408
415
|
|
|
409
|
-
|
|
416
|
+
plex_resource = Prompt.draw_dropdown(
|
|
410
417
|
title="Available Servers",
|
|
411
418
|
description="Choose a server to connect to",
|
|
412
419
|
dropdown=dropdown,
|
|
413
420
|
).value
|
|
414
421
|
|
|
422
|
+
with yaspin(text="Connecting", color="yellow") as spinner:
|
|
423
|
+
try:
|
|
424
|
+
plex_server = plex_resource.connect()
|
|
425
|
+
except NotFound as e:
|
|
426
|
+
spinner.fail(f"{Icons.FAILURE} Connection Failure")
|
|
427
|
+
description = (
|
|
428
|
+
f"Failed to connect to: {plex_resource.name} "
|
|
429
|
+
f"({plex_resource.device})\n"
|
|
430
|
+
f"Reason: [Unavailable/Not Found]"
|
|
431
|
+
)
|
|
432
|
+
raise ServerConnectionError(description) from e
|
|
433
|
+
except Exception as e:
|
|
434
|
+
spinner.fail(f"{Icons.FAILURE} Connection Failure")
|
|
435
|
+
description = (
|
|
436
|
+
f"Failed to connect to: {plex_resource.name} "
|
|
437
|
+
f"({plex_resource.device})\n"
|
|
438
|
+
f"Reason: [Unknown]"
|
|
439
|
+
)
|
|
440
|
+
raise ServerConnectionError(description) from e
|
|
441
|
+
|
|
442
|
+
spinner.ok(f"{Icons.SUCCESS} Connected")
|
|
443
|
+
|
|
444
|
+
description = f"Connected to: {plex_server}"
|
|
445
|
+
PlexUtilLogger.get_logger().debug(description)
|
|
446
|
+
return plex_server
|
|
447
|
+
|
|
415
448
|
@staticmethod
|
|
416
449
|
def confirm_plex_media(
|
|
417
450
|
title: str,
|
|
@@ -439,6 +472,31 @@ class Prompt(Static):
|
|
|
439
472
|
dropdown=dropdown,
|
|
440
473
|
).value
|
|
441
474
|
|
|
475
|
+
@staticmethod
|
|
476
|
+
def confirm_media_modification() -> bool:
|
|
477
|
+
"""
|
|
478
|
+
Prompts user whether to modify the library or the associated media
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
bool: Is media modification requested?
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
title = "Modify Media?"
|
|
486
|
+
description = (
|
|
487
|
+
"yes -> Modify a specific media item in this library\n"
|
|
488
|
+
"no -> Modify this library instead\n"
|
|
489
|
+
)
|
|
490
|
+
question = "Modify a specific media item in this library"
|
|
491
|
+
|
|
492
|
+
return Prompt.__get_toggle_response(
|
|
493
|
+
title=title,
|
|
494
|
+
description=description,
|
|
495
|
+
question=question,
|
|
496
|
+
is_from_server=False,
|
|
497
|
+
default_selection=False,
|
|
498
|
+
)
|
|
499
|
+
|
|
442
500
|
@staticmethod
|
|
443
501
|
def draw_dropdown(
|
|
444
502
|
title: str,
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import field
|
|
4
4
|
from typing import TYPE_CHECKING, cast
|
|
5
5
|
|
|
6
|
+
from plexutil.dto.dropdown_item_dto import DropdownItemDTO
|
|
7
|
+
|
|
6
8
|
if TYPE_CHECKING:
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
|
|
@@ -67,12 +69,30 @@ class TVLibrary(Library):
|
|
|
67
69
|
def update(self) -> None:
|
|
68
70
|
super().update()
|
|
69
71
|
|
|
70
|
-
def modify(self) -> None:
|
|
71
|
-
|
|
72
|
+
def modify(self, is_modify_media: bool = False) -> None:
|
|
73
|
+
is_modify_media = Prompt.confirm_media_modification()
|
|
74
|
+
super().modify(is_modify_media=is_modify_media)
|
|
72
75
|
|
|
73
76
|
def display(self, expect_input: bool = False) -> None:
|
|
74
77
|
super().display(expect_input=expect_input)
|
|
75
78
|
|
|
79
|
+
def display_media(self, expect_input: bool = False) -> Show:
|
|
80
|
+
title = "Choose Series to modify"
|
|
81
|
+
description = ""
|
|
82
|
+
dropdown = [
|
|
83
|
+
DropdownItemDTO(
|
|
84
|
+
display_name=series.title, value=series, is_default=False
|
|
85
|
+
)
|
|
86
|
+
for series in self.query()
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
return Prompt.draw_dropdown(
|
|
90
|
+
title=title,
|
|
91
|
+
description=description,
|
|
92
|
+
dropdown=dropdown,
|
|
93
|
+
expect_input=expect_input,
|
|
94
|
+
).value
|
|
95
|
+
|
|
76
96
|
def create(self) -> None:
|
|
77
97
|
super().create()
|
|
78
98
|
|
|
@@ -130,8 +130,7 @@ class LibrarySetting(Enum):
|
|
|
130
130
|
False,
|
|
131
131
|
False,
|
|
132
132
|
[],
|
|
133
|
-
|
|
134
|
-
0,
|
|
133
|
+
1,
|
|
135
134
|
)
|
|
136
135
|
|
|
137
136
|
ARTIST_BIOS = (
|
|
@@ -143,8 +142,7 @@ class LibrarySetting(Enum):
|
|
|
143
142
|
False,
|
|
144
143
|
False,
|
|
145
144
|
[],
|
|
146
|
-
|
|
147
|
-
0,
|
|
145
|
+
1,
|
|
148
146
|
)
|
|
149
147
|
|
|
150
148
|
ALBUM_REVIEWS = (
|
|
@@ -159,7 +157,6 @@ class LibrarySetting(Enum):
|
|
|
159
157
|
False,
|
|
160
158
|
False,
|
|
161
159
|
[],
|
|
162
|
-
# False,
|
|
163
160
|
0,
|
|
164
161
|
)
|
|
165
162
|
|
|
@@ -188,7 +185,7 @@ class LibrarySetting(Enum):
|
|
|
188
185
|
False,
|
|
189
186
|
False,
|
|
190
187
|
[],
|
|
191
|
-
|
|
188
|
+
1,
|
|
192
189
|
)
|
|
193
190
|
|
|
194
191
|
CONCERTS = (
|
|
@@ -200,7 +197,7 @@ class LibrarySetting(Enum):
|
|
|
200
197
|
False,
|
|
201
198
|
False,
|
|
202
199
|
[],
|
|
203
|
-
|
|
200
|
+
1,
|
|
204
201
|
)
|
|
205
202
|
|
|
206
203
|
GENRES = (
|
|
@@ -23,3 +23,7 @@ class Icons(Static):
|
|
|
23
23
|
"► " if sys.stdout.encoding.lower().startswith("utf") else "> "
|
|
24
24
|
)
|
|
25
25
|
STAR = "●" if sys.stdout.encoding.lower().startswith("utf") else "*"
|
|
26
|
+
SUCCESS = (
|
|
27
|
+
"🟢" if sys.stdout.encoding.lower().startswith("utf") else "SUCCESS"
|
|
28
|
+
)
|
|
29
|
+
FAILURE = "🔴" if sys.stdout.encoding.lower().startswith("utf") else "FAIL"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexutil
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Author-email: Carlos Florez <carlos@florez.co.uk>
|
|
5
5
|
Maintainer-email: Carlos Florez <carlos@florez.co.uk>
|
|
6
6
|
License: MIT License
|
|
@@ -42,6 +42,7 @@ Requires-Dist: peewee==3.18.3
|
|
|
42
42
|
Requires-Dist: colorlog==6.10.1
|
|
43
43
|
Requires-Dist: pyjwt[crypto]==2.10.1
|
|
44
44
|
Requires-Dist: ttkthemes==3.3.0
|
|
45
|
+
Requires-Dist: yaspin==3.4.0
|
|
45
46
|
Dynamic: license-file
|
|
46
47
|
|
|
47
48
|
# Plexutil
|
|
@@ -110,15 +111,10 @@ plexutil upload
|
|
|
110
111
|
> [!WARNING]
|
|
111
112
|
> This feature requires a graphical session (X11 or Wayland) <br>
|
|
112
113
|
|
|
113
|
-
To add songs to an existing music playlist
|
|
114
|
+
To add songs to an existing music playlist or to remove songs from an existing music playlist
|
|
114
115
|
```bash
|
|
115
|
-
plexutil
|
|
116
|
+
plexutil modify
|
|
116
117
|
```
|
|
117
|
-
To remove songs from an existing music playlist
|
|
118
|
-
```bash
|
|
119
|
-
plexutil remove_from_playlist
|
|
120
|
-
```
|
|
121
|
-
|
|
122
118
|
|
|
123
119
|
## Development
|
|
124
120
|
> [!NOTE]
|
|
@@ -59,6 +59,7 @@ src/plexutil/exception/library_poll_timeout_error.py
|
|
|
59
59
|
src/plexutil/exception/library_section_missing_error.py
|
|
60
60
|
src/plexutil/exception/library_unsupported_error.py
|
|
61
61
|
src/plexutil/exception/plex_media_missing_error.py
|
|
62
|
+
src/plexutil/exception/server_connection_error.py
|
|
62
63
|
src/plexutil/exception/unexpected_argument_error.py
|
|
63
64
|
src/plexutil/exception/unexpected_naming_pattern_error.py
|
|
64
65
|
src/plexutil/exception/user_error.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/song_music_playlist_composite_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|