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.
Files changed (86) hide show
  1. {plexutil-3.1.4 → plexutil-3.2.1}/PKG-INFO +4 -8
  2. {plexutil-3.1.4 → plexutil-3.2.1}/README.md +2 -7
  3. {plexutil-3.1.4 → plexutil-3.2.1}/pyproject.toml +2 -1
  4. {plexutil-3.1.4 → plexutil-3.2.1}/requirements.txt +1 -0
  5. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/__main__.py +10 -3
  6. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/library.py +46 -22
  7. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/movie_library.py +23 -2
  8. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/music_library.py +5 -2
  9. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/music_playlist.py +33 -6
  10. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/prompt.py +61 -3
  11. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/tv_library.py +22 -2
  12. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/library_setting.py +4 -7
  13. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/user_request.py +0 -2
  14. plexutil-3.2.1/src/plexutil/exception/server_connection_error.py +2 -0
  15. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/icons.py +4 -0
  16. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/PKG-INFO +4 -8
  17. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/SOURCES.txt +1 -0
  18. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/requires.txt +1 -0
  19. {plexutil-3.1.4 → plexutil-3.2.1}/.github/workflows/python-publish.yml +0 -0
  20. {plexutil-3.1.4 → plexutil-3.2.1}/.gitignore +0 -0
  21. {plexutil-3.1.4 → plexutil-3.2.1}/LICENSE +0 -0
  22. {plexutil-3.1.4 → plexutil-3.2.1}/MANIFEST.in +0 -0
  23. {plexutil-3.1.4 → plexutil-3.2.1}/git-hooks/commit-msg +0 -0
  24. {plexutil-3.1.4 → plexutil-3.2.1}/git-hooks/pre-commit +0 -0
  25. {plexutil-3.1.4 → plexutil-3.2.1}/init.sh +0 -0
  26. {plexutil-3.1.4 → plexutil-3.2.1}/ruff.toml +0 -0
  27. {plexutil-3.1.4 → plexutil-3.2.1}/setup.cfg +0 -0
  28. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/__init__.py +0 -0
  29. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/config/log_config.yaml +0 -0
  30. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/__init__.py +0 -0
  31. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/auth.py +0 -0
  32. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/core/library_factory.py +0 -0
  33. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/__init__.py +0 -0
  34. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/bootstrap_paths_dto.py +0 -0
  35. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/dropdown_item_dto.py +0 -0
  36. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/library_setting_dto.py +0 -0
  37. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/movie_dto.py +0 -0
  38. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/music_playlist_dto.py +0 -0
  39. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/song_dto.py +0 -0
  40. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/tv_episode_dto.py +0 -0
  41. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/dto/tv_series_dto.py +0 -0
  42. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/__init__.py +0 -0
  43. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/agent.py +0 -0
  44. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/file_type.py +0 -0
  45. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/language.py +0 -0
  46. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/library_type.py +0 -0
  47. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/scanner.py +0 -0
  48. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/enums/server_setting.py +0 -0
  49. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/__init__.py +0 -0
  50. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/auth_error.py +0 -0
  51. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/bootstrap_error.py +0 -0
  52. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/database_connection_error.py +0 -0
  53. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/device_error.py +0 -0
  54. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/entity_not_found_error.py +0 -0
  55. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_illegal_state_error.py +0 -0
  56. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_op_error.py +0 -0
  57. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_poll_timeout_error.py +0 -0
  58. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_section_missing_error.py +0 -0
  59. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/library_unsupported_error.py +0 -0
  60. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/plex_media_missing_error.py +0 -0
  61. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/unexpected_argument_error.py +0 -0
  62. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/unexpected_naming_pattern_error.py +0 -0
  63. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/exception/user_error.py +0 -0
  64. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/graphical/__init__.py +0 -0
  65. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/graphical/selection_window.py +0 -0
  66. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/mapper/__init__.py +0 -0
  67. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/mapper/music_playlist_mapper.py +0 -0
  68. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/mapper/song_mapper.py +0 -0
  69. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/__init__.py +0 -0
  70. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/music_playlist_entity.py +0 -0
  71. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/song_entity.py +0 -0
  72. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/model/song_music_playlist_entity.py +0 -0
  73. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/plex_util_logger.py +0 -0
  74. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/__init__.py +0 -0
  75. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/db_manager.py +0 -0
  76. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/music_playlist_service.py +0 -0
  77. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/song_music_playlist_composite_service.py +0 -0
  78. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/service/song_service.py +0 -0
  79. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/static.py +0 -0
  80. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/__init__.py +0 -0
  81. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/file_importer.py +0 -0
  82. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/plex_ops.py +0 -0
  83. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil/util/query_builder.py +0 -0
  84. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/dependency_links.txt +0 -0
  85. {plexutil-3.1.4 → plexutil-3.2.1}/src/plexutil.egg-info/entry_points.txt +0 -0
  86. {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.4
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 add_to_playlist
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 add_to_playlist
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.4"
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]
@@ -8,3 +8,4 @@ peewee==3.18.3
8
8
  colorlog==6.10.1
9
9
  pyjwt[crypto]==2.10.1
10
10
  ttkthemes==3.3.0
11
+ yaspin==3.4.0
@@ -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
- self.assign_language(default=self.language, is_from_server=True)
124
- self.get_section().edit(
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
- PlexOps.set_library_settings(
131
- section=self.get_section(),
132
- settings=[x.to_dto(is_from_server=True) for x in settings],
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
- super().modify()
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.ADD_TO_PLAYLIST,
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
- raise NotImplementedError
123
+ # None of MusicPlaylist operations benefit from a refresh/reload
124
+ return
125
125
 
126
- def modify(self) -> None:
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.ADD_TO_PLAYLIST
154
- or self.user_request is UserRequest.REMOVE_FROM_PLAYLIST
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]) -> 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
- MyPlexResource: The chosen Plex Media Server
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
- return Prompt.draw_dropdown(
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
- super().modify()
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
- # False,
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
- # False,
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
- 0,
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
- 0,
200
+ 1,
204
201
  )
205
202
 
206
203
  GENRES = (
@@ -12,8 +12,6 @@ class UserRequest(Enum):
12
12
  UPLOAD = "upload"
13
13
  DISPLAY = "display"
14
14
  UPDATE = "update"
15
- ADD_TO_PLAYLIST = "add_to_playlist"
16
- REMOVE_FROM_PLAYLIST = "remove_from_playlist"
17
15
 
18
16
  @staticmethod
19
17
  # Forward Reference used here in type hint
@@ -0,0 +1,2 @@
1
+ class ServerConnectionError(Exception):
2
+ pass
@@ -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.4
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 add_to_playlist
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
@@ -7,6 +7,7 @@ peewee==3.18.3
7
7
  colorlog==6.10.1
8
8
  pyjwt[crypto]==2.10.1
9
9
  ttkthemes==3.3.0
10
+ yaspin==3.4.0
10
11
 
11
12
  [:sys_platform == "win32"]
12
13
  pywin32==311
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes