From 5c115dec68effbaf8d89e8a951e8e5f22e6f7afa Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 11 May 2024 09:51:05 -0700 Subject: [PATCH 001/127] Fix uploading MacOS installer release asset --- .github/workflows/publish-installers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-installers.yml b/.github/workflows/publish-installers.yml index 5e218a0c..e3f7fe85 100644 --- a/.github/workflows/publish-installers.yml +++ b/.github/workflows/publish-installers.yml @@ -143,7 +143,7 @@ jobs: prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }} files: | Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe - Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg + Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-universal.pkg discord: name: Discord Notification From ee0b4c0602c6f706bc88f78859e930ea540295ae Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 12 May 2024 22:38:05 -0700 Subject: [PATCH 002/127] Add artProvider and thumbProvider to exporter fields --- plexpy/exporter.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/plexpy/exporter.py b/plexpy/exporter.py index ef6e0da7..8dcefbc1 100644 --- a/plexpy/exporter.py +++ b/plexpy/exporter.py @@ -148,6 +148,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'audienceRating': None, 'audienceRatingImage': None, 'chapters': { @@ -384,6 +385,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -406,6 +408,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'audienceRating': None, 'audienceRatingImage': None, 'audioLanguage': None, @@ -472,6 +475,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -491,6 +495,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'audioLanguage': None, 'collections': { 'id': None, @@ -534,6 +539,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -552,6 +558,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'audienceRating': None, 'audienceRatingImage': None, 'chapters': { @@ -792,6 +799,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -814,6 +822,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'collections': { 'id': None, 'tag': None @@ -866,6 +875,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -882,6 +892,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'collections': { 'id': None, 'tag': None @@ -944,6 +955,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -1223,6 +1235,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), + 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), 'childCount': None, 'collectionFilterBasedOnUser': None, 'collectionMode': None, @@ -1253,6 +1266,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), + 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), 'title': None, 'titleSort': None, 'type': None, @@ -2239,15 +2253,25 @@ class Export(object): media = helpers.get_attrs_to_dict(item, attrs) return any(vs.get('hdr') for p in media.get('parts', []) for vs in p.get('videoStreams', [])) + def _get_cached_images(self, item, image): + if image == 'art': + if not hasattr(item, '_arts'): + item._arts = item.arts() + return getattr(item, '_arts', []) + else: + if not hasattr(item, '_posters'): + item._posters = item.posters() + return getattr(item, '_posters', []) + def get_image(self, item, image): media_type = item.type rating_key = item.ratingKey export_image = True if self.thumb_level == 1 or self.art_level == 1: - posters = item.arts() if image == 'art' else item.posters() - export_image = any(poster.selected and poster.ratingKey.startswith('upload://') - for poster in posters) + images = self._get_cached_images(item, image) + export_image = any(im.selected and im.ratingKey.startswith('upload://') + for im in images) elif self.thumb_level == 2 or self.art_level == 2: export_image = any(field.locked and field.name == image for field in item.fields) @@ -2293,6 +2317,12 @@ class Export(object): return os.path.join(os.path.basename(dirpath), filename) + def get_image_attr(self, item, image, attr): + images = self._get_cached_images(item, image) + selected = next((im for im in images if im.selected), None) + if selected: + return getattr(selected, attr) + class ExportObject(Export): def __init__(self, export, obj): From 68bf1c70f71a9ee047ff69200b57f3db17a2cd87 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 13 May 2024 20:56:40 -0700 Subject: [PATCH 003/127] Overwrite image provider export field for uploaded assets --- plexpy/exporter.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/plexpy/exporter.py b/plexpy/exporter.py index 8dcefbc1..08ea1176 100644 --- a/plexpy/exporter.py +++ b/plexpy/exporter.py @@ -148,7 +148,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'audienceRating': None, 'audienceRatingImage': None, 'chapters': { @@ -385,7 +385,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'thumb'), 'title': None, 'titleSort': None, 'type': None, @@ -408,7 +408,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'audienceRating': None, 'audienceRatingImage': None, 'audioLanguage': None, @@ -475,7 +475,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'thumb'), 'title': None, 'titleSort': None, 'type': None, @@ -495,7 +495,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'audioLanguage': None, 'collections': { 'id': None, @@ -539,7 +539,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'thumb'), 'title': None, 'titleSort': None, 'type': None, @@ -558,7 +558,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'audienceRating': None, 'audienceRatingImage': None, 'chapters': { @@ -799,7 +799,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'thumb'), 'title': None, 'titleSort': None, 'type': None, @@ -822,7 +822,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'collections': { 'id': None, 'tag': None @@ -875,7 +875,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'thumb'), 'title': None, 'titleSort': None, 'type': None, @@ -892,7 +892,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'collections': { 'id': None, 'tag': None @@ -955,7 +955,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'thumb'), 'title': None, 'titleSort': None, 'type': None, @@ -1235,7 +1235,7 @@ class Export(object): 'art': None, 'artBlurHash': None, 'artFile': lambda o: self.get_image(o, 'art'), - 'artProvider': lambda o: self.get_image_attr(o, 'art', 'provider'), + 'artProvider': lambda o: self.get_image_provider(o, 'art'), 'childCount': None, 'collectionFilterBasedOnUser': None, 'collectionMode': None, @@ -1266,7 +1266,7 @@ class Export(object): 'thumb': None, 'thumbBlurHash': None, 'thumbFile': lambda o: self.get_image(o, 'thumb'), - 'thumbProvider': lambda o: self.get_image_attr(o, 'thumb', 'provider'), + 'thumbProvider': lambda o: self.get_image_provider(o, 'poster'), 'title': None, 'titleSort': None, 'type': None, @@ -2262,6 +2262,10 @@ class Export(object): if not hasattr(item, '_posters'): item._posters = item.posters() return getattr(item, '_posters', []) + + def _get_selected_image(self, item, image): + images = self._get_cached_images(item, image) + return next((im for im in images if im.selected), None) def get_image(self, item, image): media_type = item.type @@ -2269,9 +2273,8 @@ class Export(object): export_image = True if self.thumb_level == 1 or self.art_level == 1: - images = self._get_cached_images(item, image) - export_image = any(im.selected and im.ratingKey.startswith('upload://') - for im in images) + selected = self._get_selected_image(item, image) + export_image = selected and selected.ratingKey.startswith('upload://') elif self.thumb_level == 2 or self.art_level == 2: export_image = any(field.locked and field.name == image for field in item.fields) @@ -2317,11 +2320,10 @@ class Export(object): return os.path.join(os.path.basename(dirpath), filename) - def get_image_attr(self, item, image, attr): - images = self._get_cached_images(item, image) - selected = next((im for im in images if im.selected), None) + def get_image_provider(self, item, image): + selected = self._get_selected_image(item, image) if selected: - return getattr(selected, attr) + return 'upload' if selected.ratingKey.startswith('upload://') else selected.provider class ExportObject(Export): From dc9e77811108353b847c8b2a15735aa2805be352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 11:17:01 -0700 Subject: [PATCH 004/127] Bump platformdirs from 4.2.1 to 4.2.2 (#2324) * Bump platformdirs from 4.2.1 to 4.2.2 Bumps [platformdirs](https://github.com/platformdirs/platformdirs) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/platformdirs/platformdirs/releases) - [Changelog](https://github.com/platformdirs/platformdirs/blob/main/CHANGES.rst) - [Commits](https://github.com/platformdirs/platformdirs/compare/4.2.1...4.2.2) --- updated-dependencies: - dependency-name: platformdirs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update platformdirs==4.2.2 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci] --- lib/platformdirs/android.py | 45 +++++++++++++++++++++++++++++-------- lib/platformdirs/version.py | 4 ++-- requirements.txt | 2 +- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/lib/platformdirs/android.py b/lib/platformdirs/android.py index fefafd32..afd3141c 100644 --- a/lib/platformdirs/android.py +++ b/lib/platformdirs/android.py @@ -6,7 +6,7 @@ import os import re import sys from functools import lru_cache -from typing import cast +from typing import TYPE_CHECKING, cast from .api import PlatformDirsABC @@ -117,16 +117,33 @@ class Android(PlatformDirsABC): @lru_cache(maxsize=1) -def _android_folder() -> str | None: +def _android_folder() -> str | None: # noqa: C901, PLR0912 """:return: base folder for the Android OS or None if it cannot be found""" - try: - # First try to get a path to android app via pyjnius - from jnius import autoclass # noqa: PLC0415 + result: str | None = None + # type checker isn't happy with our "import android", just don't do this when type checking see + # https://stackoverflow.com/a/61394121 + if not TYPE_CHECKING: + try: + # First try to get a path to android app using python4android (if available)... + from android import mActivity # noqa: PLC0415 - context = autoclass("android.content.Context") - result: str | None = context.getFilesDir().getParentFile().getAbsolutePath() - except Exception: # noqa: BLE001 - # if fails find an android folder looking a path on the sys.path + context = cast("android.content.Context", mActivity.getApplicationContext()) # noqa: F821 + result = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + result = None + if result is None: + try: + # ...and fall back to using plain pyjnius, if python4android isn't available or doesn't deliver any useful + # result... + from jnius import autoclass # noqa: PLC0415 + + context = autoclass("android.content.Context") + result = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + result = None + if result is None: + # and if that fails, too, find an android folder looking at path on the sys.path + # warning: only works for apps installed under /data, not adopted storage etc. pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files") for path in sys.path: if pattern.match(path): @@ -134,6 +151,16 @@ def _android_folder() -> str | None: break else: result = None + if result is None: + # one last try: find an android folder looking at path on the sys.path taking adopted storage paths into + # account + pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files") + for path in sys.path: + if pattern.match(path): + result = path.split("/files")[0] + break + else: + result = None return result diff --git a/lib/platformdirs/version.py b/lib/platformdirs/version.py index c418cd0c..6483ddce 100644 --- a/lib/platformdirs/version.py +++ b/lib/platformdirs/version.py @@ -12,5 +12,5 @@ __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE -__version__ = version = '4.2.1' -__version_tuple__ = version_tuple = (4, 2, 1) +__version__ = version = '4.2.2' +__version_tuple__ = version_tuple = (4, 2, 2) diff --git a/requirements.txt b/requirements.txt index f2d316c2..4e95ca75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ MarkupSafe==2.1.5 musicbrainzngs==0.7.1 packaging==24.0 paho-mqtt==2.1.0 -platformdirs==4.2.1 +platformdirs==4.2.2 plexapi==4.15.12 portend==3.2.0 profilehooks==1.12.0 From 37ffe68ce2e0790e25b20c36aff6ccd73312ef2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 11:17:13 -0700 Subject: [PATCH 005/127] Bump mako from 1.3.3 to 1.3.5 (#2325) * Bump mako from 1.3.3 to 1.3.5 Bumps [mako](https://github.com/sqlalchemy/mako) from 1.3.3 to 1.3.5. - [Release notes](https://github.com/sqlalchemy/mako/releases) - [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/mako/commits) --- updated-dependencies: - dependency-name: mako dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update mako==1.3.5 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci] --- lib/mako/__init__.py | 2 +- lib/mako/pyparser.py | 4 ---- requirements.txt | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/mako/__init__.py b/lib/mako/__init__.py index 5811e68c..0ac3dd68 100644 --- a/lib/mako/__init__.py +++ b/lib/mako/__init__.py @@ -5,4 +5,4 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php -__version__ = "1.3.3" +__version__ = "1.3.5" diff --git a/lib/mako/pyparser.py b/lib/mako/pyparser.py index b25ef6e4..714e0043 100644 --- a/lib/mako/pyparser.py +++ b/lib/mako/pyparser.py @@ -92,8 +92,6 @@ class FindIdentifiers(_ast_util.NodeVisitor): def visit_ListComp(self, node): if self.in_function: - if not isinstance(node.elt, _ast.Name): - self.visit(node.elt) for comp in node.generators: self.visit(comp.iter) else: @@ -103,8 +101,6 @@ class FindIdentifiers(_ast_util.NodeVisitor): def visit_DictComp(self, node): if self.in_function: - if not isinstance(node.key, _ast.Name): - self.visit(node.elt) for comp in node.generators: self.visit(comp.iter) else: diff --git a/requirements.txt b/requirements.txt index 4e95ca75..49e2246c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ importlib-metadata==7.1.0 importlib-resources==6.4.0 git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois IPy==1.01 -Mako==1.3.3 +Mako==1.3.5 MarkupSafe==2.1.5 musicbrainzngs==0.7.1 packaging==24.0 From c505e26656cab55b70adec83fd3401f30a49127f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 11:17:26 -0700 Subject: [PATCH 006/127] Bump zipp from 3.18.1 to 3.18.2 (#2326) * Bump zipp from 3.18.1 to 3.18.2 Bumps [zipp](https://github.com/jaraco/zipp) from 3.18.1 to 3.18.2. - [Release notes](https://github.com/jaraco/zipp/releases) - [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst) - [Commits](https://github.com/jaraco/zipp/compare/v3.18.1...v3.18.2) --- updated-dependencies: - dependency-name: zipp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update zipp==3.18.2 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci] --- lib/zipp/__init__.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/zipp/__init__.py b/lib/zipp/__init__.py index 6d05d9a0..df3293aa 100644 --- a/lib/zipp/__init__.py +++ b/lib/zipp/__init__.py @@ -263,7 +263,7 @@ class Path: >>> str(path.parent) 'mem' - If the zipfile has no filename, such attribtues are not + If the zipfile has no filename, such attributes are not valid and accessing them will raise an Exception. >>> zf.filename = None diff --git a/requirements.txt b/requirements.txt index 49e2246c..3d624c3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ urllib3<2 webencodings==0.5.1 websocket-client==1.8.0 xmltodict==0.13.0 -zipp==3.18.1 +zipp==3.18.2 # configobj==5.1.0 # sgmllib3k==1.0.0 From 8419eee4b207fa8585c1925e761a3c5b446963d0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Fri, 17 May 2024 15:41:59 -0700 Subject: [PATCH 007/127] Catch exception when decoding server response message --- plexpy/request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plexpy/request.py b/plexpy/request.py index c2f0967f..82c8cee0 100644 --- a/plexpy/request.py +++ b/plexpy/request.py @@ -308,7 +308,10 @@ def server_message(response, return_msg=False): message = response.content.strip() if message: - message = str(message, 'utf-8', 'replace') + try: + message = message.decode('utf-8', errors='replace') + except (UnicodeDecodeError, AttributeError): + pass # Truncate message if it is too long. if len(message) > 150: From 8396a04ce86e8e1974c0eda0fc417b30c2cedfb5 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 18 May 2024 13:52:52 -0700 Subject: [PATCH 008/127] Update plexapi==4.15.13 --- lib/plexapi/audio.py | 10 ++++++++ lib/plexapi/base.py | 2 +- lib/plexapi/collection.py | 4 +++ lib/plexapi/const.py | 2 +- lib/plexapi/library.py | 4 +-- lib/plexapi/mixins.py | 52 ++++++++++++++++++++++++++++++++------- lib/plexapi/myplex.py | 2 +- lib/plexapi/playlist.py | 27 +++++++++++--------- lib/plexapi/video.py | 4 +++ requirements.txt | 2 +- 10 files changed, 83 insertions(+), 26 deletions(-) diff --git a/lib/plexapi/audio.py b/lib/plexapi/audio.py index 0eb397cc..8f84f3be 100644 --- a/lib/plexapi/audio.py +++ b/lib/plexapi/audio.py @@ -181,6 +181,7 @@ class Artist( TYPE (str): 'artist' albumSort (int): Setting that indicates how albums are sorted for the artist (-1 = Library default, 0 = Newest first, 1 = Oldest first, 2 = By name). + audienceRating (float): Audience rating. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. countries (List<:class:`~plexapi.media.Country`>): List country objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. @@ -188,6 +189,7 @@ class Artist( key (str): API URL (/library/metadata/). labels (List<:class:`~plexapi.media.Label`>): List of label objects. locations (List): List of folder paths where the artist is found on disk. + rating (float): Artist rating (7.9; 9.8; 8.1). similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects. theme (str): URL to theme resource (/library/metadata//theme/). @@ -199,6 +201,7 @@ class Artist( """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) self.albumSort = utils.cast(int, data.attrib.get('albumSort', '-1')) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.collections = self.findItems(data, media.Collection) self.countries = self.findItems(data, media.Country) self.genres = self.findItems(data, media.Genre) @@ -206,6 +209,7 @@ class Artist( self.key = self.key.replace('/children', '') # FIX_BUG_50 self.labels = self.findItems(data, media.Label) self.locations = self.listAttrs(data, 'path', etag='Location') + self.rating = utils.cast(float, data.attrib.get('rating')) self.similar = self.findItems(data, media.Similar) self.styles = self.findItems(data, media.Style) self.theme = data.attrib.get('theme') @@ -301,6 +305,7 @@ class Album( Attributes: TAG (str): 'Directory' TYPE (str): 'album' + audienceRating (float): Audience rating. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. formats (List<:class:`~plexapi.media.Format`>): List of format objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. @@ -329,6 +334,7 @@ class Album( def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.collections = self.findItems(data, media.Collection) self.formats = self.findItems(data, media.Format) self.genres = self.findItems(data, media.Genre) @@ -426,6 +432,7 @@ class Track( Attributes: TAG (str): 'Directory' TYPE (str): 'track' + audienceRating (float): Audience rating. chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. chapterSource (str): Unknown collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. @@ -451,6 +458,7 @@ class Track( parentThumb (str): URL to album thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the album for the track. primaryExtraKey (str) API URL for the primary extra for the track. + rating (float): Track rating (7.9; 9.8; 8.1). ratingCount (int): Number of listeners who have scrobbled this track, as reported by Last.fm. skipCount (int): Number of times the track has been skipped. sourceURI (str): Remote server URI (server:///com.plexapp.plugins.library) @@ -465,6 +473,7 @@ class Track( """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) Playable._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.chapters = self.findItems(data, media.Chapter) self.chapterSource = data.attrib.get('chapterSource') self.collections = self.findItems(data, media.Collection) @@ -488,6 +497,7 @@ class Track( self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') self.primaryExtraKey = data.attrib.get('primaryExtraKey') + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.skipCount = utils.cast(int, data.attrib.get('skipCount')) self.sourceURI = data.attrib.get('source') # remote playlist item diff --git a/lib/plexapi/base.py b/lib/plexapi/base.py index c08334f9..9c735373 100644 --- a/lib/plexapi/base.py +++ b/lib/plexapi/base.py @@ -253,7 +253,7 @@ class PlexObject: fetchItem(ekey, viewCount__gte=0) fetchItem(ekey, Media__container__in=["mp4", "mkv"]) - fetchItem(ekey, guid__regex=r"com\\.plexapp\\.agents\\.(imdb|themoviedb)://|tt\d+") + fetchItem(ekey, guid__regex=r"com\\.plexapp\\.agents\\.(imdb|themoviedb)://|tt\\d+") fetchItem(ekey, guid__id__regex=r"(imdb|tmdb|tvdb)://") fetchItem(ekey, Media__Part__file__startswith="D:\\Movies") diff --git a/lib/plexapi/collection.py b/lib/plexapi/collection.py index 5f591a4a..d71ddf2f 100644 --- a/lib/plexapi/collection.py +++ b/lib/plexapi/collection.py @@ -29,6 +29,7 @@ class Collection( addedAt (datetime): Datetime the collection was added to the library. art (str): URL to artwork image (/library/metadata//art/). artBlurHash (str): BlurHash string for artwork image. + audienceRating (float): Audience rating. childCount (int): Number of items in the collection. collectionFilterBasedOnUser (int): Which user's activity is used for the collection filtering. collectionMode (int): How the items in the collection are displayed. @@ -47,6 +48,7 @@ class Collection( librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. maxYear (int): Maximum year for the items in the collection. minYear (int): Minimum year for the items in the collection. + rating (float): Collection rating (7.9; 9.8; 8.1). ratingCount (int): The number of ratings. ratingKey (int): Unique key identifying the collection. smart (bool): True if the collection is a smart collection. @@ -69,6 +71,7 @@ class Collection( self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.childCount = utils.cast(int, data.attrib.get('childCount')) self.collectionFilterBasedOnUser = utils.cast(int, data.attrib.get('collectionFilterBasedOnUser', '0')) self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1')) @@ -87,6 +90,7 @@ class Collection( self.librarySectionTitle = data.attrib.get('librarySectionTitle') self.maxYear = utils.cast(int, data.attrib.get('maxYear')) self.minYear = utils.cast(int, data.attrib.get('minYear')) + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.smart = utils.cast(bool, data.attrib.get('smart', '0')) diff --git a/lib/plexapi/const.py b/lib/plexapi/const.py index c987e305..7568344e 100644 --- a/lib/plexapi/const.py +++ b/lib/plexapi/const.py @@ -4,6 +4,6 @@ # Library version MAJOR_VERSION = 4 MINOR_VERSION = 15 -PATCH_VERSION = 12 +PATCH_VERSION = 13 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/lib/plexapi/library.py b/lib/plexapi/library.py index 662b5462..f1bf5375 100644 --- a/lib/plexapi/library.py +++ b/lib/plexapi/library.py @@ -226,7 +226,7 @@ class Library(PlexObject): section.deleteMediaPreviews() return self - def add(self, name='', type='', agent='', scanner='', location='', language='en', *args, **kwargs): + def add(self, name='', type='', agent='', scanner='', location='', language='en-US', *args, **kwargs): """ Simplified add for the most common options. Parameters: @@ -234,7 +234,7 @@ class Library(PlexObject): agent (str): Example com.plexapp.agents.imdb type (str): movie, show, # check me location (str or list): /path/to/files, ["/path/to/files", "/path/to/morefiles"] - language (str): Two letter language fx en + language (str): Four letter language code (e.g. en-US) kwargs (dict): Advanced options should be passed as a dict. where the id is the key. **Photo Preferences** diff --git a/lib/plexapi/mixins.py b/lib/plexapi/mixins.py index 60c24e26..8571ba63 100644 --- a/lib/plexapi/mixins.py +++ b/lib/plexapi/mixins.py @@ -567,6 +567,19 @@ class AddedAtMixin(EditFieldMixin): return self.editField('addedAt', addedAt, locked=locked) +class AudienceRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an audience rating. """ + + def editAudienceRating(self, audienceRating, locked=True): + """ Edit the audience rating. + + Parameters: + audienceRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('audienceRating', audienceRating, locked=locked) + + class ContentRatingMixin(EditFieldMixin): """ Mixin for Plex objects that can have a content rating. """ @@ -580,6 +593,19 @@ class ContentRatingMixin(EditFieldMixin): return self.editField('contentRating', contentRating, locked=locked) +class CriticRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a critic rating. """ + + def editCriticRating(self, criticRating, locked=True): + """ Edit the critic rating. + + Parameters: + criticRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('rating', criticRating, locked=locked) + + class EditionTitleMixin(EditFieldMixin): """ Mixin for Plex objects that can have an edition title. """ @@ -751,7 +777,7 @@ class UserRatingMixin(EditFieldMixin): """ Edit the user rating. Parameters: - userRating (int): The new value. + userRating (float): The new value. locked (bool): True (default) to lock the field, False to unlock the field. """ return self.editField('userRating', userRating, locked=locked) @@ -1145,7 +1171,8 @@ class WatchlistMixin: class MovieEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, EditionTitleMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin ): @@ -1154,7 +1181,8 @@ class MovieEditMixins( class ShowEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, ): @@ -1163,7 +1191,8 @@ class ShowEditMixins( class SeasonEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, LabelMixin ): pass @@ -1171,7 +1200,8 @@ class SeasonEditMixins( class EpisodeEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, DirectorMixin, LabelMixin, WriterMixin ): pass @@ -1179,7 +1209,8 @@ class EpisodeEditMixins( class ArtistEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin ): pass @@ -1187,7 +1218,8 @@ class ArtistEditMixins( class AlbumEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin ): pass @@ -1195,7 +1227,8 @@ class AlbumEditMixins( class TrackEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin ): pass @@ -1218,7 +1251,8 @@ class PhotoEditMixins( class CollectionEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, LabelMixin ): pass diff --git a/lib/plexapi/myplex.py b/lib/plexapi/myplex.py index 8d697924..740b2398 100644 --- a/lib/plexapi/myplex.py +++ b/lib/plexapi/myplex.py @@ -99,7 +99,7 @@ class MyPlexAccount(PlexObject): EXISTINGUSER = 'https://plex.tv/api/home/users?invitedEmail={username}' # post with data FRIENDSERVERS = 'https://plex.tv/api/servers/{machineId}/shared_servers/{serverId}' # put with data PLEXSERVERS = 'https://plex.tv/api/servers/{machineId}' # get - FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete + FRIENDUPDATE = 'https://plex.tv/api/v2/sharings/{userId}' # put with args, delete HOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete, put MANAGEDHOMEUSER = 'https://plex.tv/api/v2/home/users/restricted/{userId}' # put SIGNIN = 'https://plex.tv/api/v2/users/signin' # post with auth diff --git a/lib/plexapi/playlist.py b/lib/plexapi/playlist.py index a50b6200..dda3bdf5 100644 --- a/lib/plexapi/playlist.py +++ b/lib/plexapi/playlist.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import re +from itertools import groupby from pathlib import Path from urllib.parse import quote_plus, unquote @@ -212,19 +213,23 @@ class Playlist( if items and not isinstance(items, (list, tuple)): items = [items] - ratingKeys = [] - for item in items: - if item.listType != self.playlistType: # pragma: no cover - raise BadRequest(f'Can not mix media types when building a playlist: ' - f'{self.playlistType} and {item.listType}') - ratingKeys.append(str(item.ratingKey)) + # Group items by server to maintain order when adding items from multiple servers + for server, _items in groupby(items, key=lambda item: item._server): - ratingKeys = ','.join(ratingKeys) - uri = f'{self._server._uriRoot()}/library/metadata/{ratingKeys}' + ratingKeys = [] + for item in _items: + if item.listType != self.playlistType: # pragma: no cover + raise BadRequest(f'Can not mix media types when building a playlist: ' + f'{self.playlistType} and {item.listType}') + ratingKeys.append(str(item.ratingKey)) + + ratingKeys = ','.join(ratingKeys) + uri = f'{server._uriRoot()}/library/metadata/{ratingKeys}' + + args = {'uri': uri} + key = f"{self.key}/items{utils.joinArgs(args)}" + self._server.query(key, method=self._server._session.put) - args = {'uri': uri} - key = f"{self.key}/items{utils.joinArgs(args)}" - self._server.query(key, method=self._server._session.put) return self @deprecated('use "removeItems" instead') diff --git a/lib/plexapi/video.py b/lib/plexapi/video.py index 727ba0f8..609f57f6 100644 --- a/lib/plexapi/video.py +++ b/lib/plexapi/video.py @@ -713,6 +713,7 @@ class Season( Attributes: TAG (str): 'Directory' TYPE (str): 'season' + audienceRating (float): Audience rating. audioLanguage (str): Setting that indicates the preferred audio language. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. @@ -729,6 +730,7 @@ class Season( parentTheme (str): URL to show theme resource (/library/metadata//theme/). parentThumb (str): URL to show thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the show for the season. + rating (float): Season rating (7.9; 9.8; 8.1). ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects. subtitleLanguage (str): Setting that indicates the preferred subtitle language. subtitleMode (int): Setting that indicates the auto-select subtitle mode. @@ -743,6 +745,7 @@ class Season( def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audioLanguage = data.attrib.get('audioLanguage', '') self.collections = self.findItems(data, media.Collection) self.guids = self.findItems(data, media.Guid) @@ -759,6 +762,7 @@ class Season( self.parentTheme = data.attrib.get('parentTheme') self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratings = self.findItems(data, media.Rating) self.subtitleLanguage = data.attrib.get('subtitleLanguage', '') self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1')) diff --git a/requirements.txt b/requirements.txt index 3d624c3a..6edced4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ musicbrainzngs==0.7.1 packaging==24.0 paho-mqtt==2.1.0 platformdirs==4.2.2 -plexapi==4.15.12 +plexapi==4.15.13 portend==3.2.0 profilehooks==1.12.0 PyJWT==2.8.0 From 795d568df2431c14ae4a2d1e1ee7716c94d07e0e Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 18 May 2024 13:56:06 -0700 Subject: [PATCH 009/127] v2.14.2 --- CHANGELOG.md | 13 ++++--------- plexpy/version.py | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48e02c60..e876fbb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,6 @@ # Changelog -## v2.14.1-beta (2024-05-11) - -* UI: - * Fix: Users without access to Plex server not showing as inactive. -* Other: - * Removed: Python 2 compatibility code. (#2098, #2226) (Thanks @zdimension) - - -## v2.14.0-beta (2024-04-19) +## v2.14.2 (2024-05-18) * History: * Fix: Live TV activity not logging to history. @@ -27,9 +19,11 @@ * New: Added slug attribute to exporter fields. * New: Added track genres to exporter fields. * New: Added playlist source URI to exporter fields. + * New: Added artProvider and thumbProvider to exporter fields. * UI: * Fix: Mask deleted usernames in the logs. * Fix: Live TV watch stats not showing on the media info page. + * Fix: Users without access to Plex server not showing as inactive. * Removed: Deprecated synced item pages. * Removed: Anonymous redirect settings. Links now use browser no-referrer policy instead. * API: @@ -41,6 +35,7 @@ * Other: * Change: Login cookie expires changed to max-age. * Change: Improved key generation for login password. It is recommended to reenter your HTTP Password in the settings after upgrading. + * Removed: Python 2 compatibility code. (#2098, #2226) (Thanks @zdimension) ## v2.13.4 (2023-12-07) diff --git a/plexpy/version.py b/plexpy/version.py index cf3944db..698d7d45 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -15,5 +15,5 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -PLEXPY_BRANCH = "beta" -PLEXPY_RELEASE_VERSION = "v2.14.1-beta" \ No newline at end of file +PLEXPY_BRANCH = "master" +PLEXPY_RELEASE_VERSION = "v2.14.2" \ No newline at end of file From 73cfa8e0c09191bb66365807b62a51e2056d6ad4 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 18 May 2024 14:29:53 -0700 Subject: [PATCH 010/127] Add `git clean` to reset git install --- plexpy/versioncheck.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plexpy/versioncheck.py b/plexpy/versioncheck.py index 442dcc23..2c161dd3 100644 --- a/plexpy/versioncheck.py +++ b/plexpy/versioncheck.py @@ -394,6 +394,7 @@ def reset_git_install(): output, err = runGit('branch -u {}/{}'.format(plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH)) output, err = runGit('reset --hard {}'.format(common.RELEASE)) + _, _ = runGit('clean -fd') if not output: logger.error('Unable to reset Tautulli installation.') From dcdf5a2992228ee133373c8497111707df54615f Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 May 2024 10:40:00 -0700 Subject: [PATCH 011/127] Fix SQLite quotes for history date filters --- plexpy/webserve.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 444d5a68..ec98baf2 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1997,15 +1997,15 @@ class WebInterface(object): if 'start_date' in kwargs: start_date = helpers.split_strip(kwargs.pop('start_date', '')) if start_date: - custom_where.append(['strftime("%Y-%m-%d", datetime(started, "unixepoch", "localtime"))', start_date]) + custom_where.append(["strftime('%Y-%m-%d', datetime(started, 'unixepoch', 'localtime'))", start_date]) if 'before' in kwargs: before = helpers.split_strip(kwargs.pop('before', '')) if before: - custom_where.append(['strftime("%Y-%m-%d", datetime(started, "unixepoch", "localtime")) <', before]) + custom_where.append(["strftime('%Y-%m-%d', datetime(started, 'unixepoch', 'localtime')) <", before]) if 'after' in kwargs: after = helpers.split_strip(kwargs.pop('after', '')) if after: - custom_where.append(['strftime("%Y-%m-%d", datetime(started, "unixepoch", "localtime")) >', after]) + custom_where.append(["strftime('%Y-%m-%d', datetime(started, 'unixepoch', 'localtime')) >", after]) if 'reference_id' in kwargs: reference_id = helpers.split_strip(kwargs.pop('reference_id', '')) if reference_id: From 5778672dabfbed37fc9dbef94ca75570477dbbd7 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 25 May 2024 18:41:17 -0700 Subject: [PATCH 012/127] Fix webserver restarting --- plexpy/webstart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plexpy/webstart.py b/plexpy/webstart.py index b02e9ec1..62ab41ae 100644 --- a/plexpy/webstart.py +++ b/plexpy/webstart.py @@ -51,6 +51,7 @@ def start(): def stop(): logger.info("Tautulli WebStart :: Stopping Tautulli web server...") cherrypy.engine.exit() + cherrypy.server.httpserver = None def restart(): From 912fd75a2fce2a75ce6181e0e6b8c92d75a156aa Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:14:04 -0700 Subject: [PATCH 013/127] Remove pms_is_remote setting * Automatically determine if a server is local or remote --- data/interfaces/default/settings.html | 15 -------------- data/interfaces/default/welcome.html | 20 +----------------- plexpy/plextv.py | 30 +++++++++++---------------- plexpy/webserve.py | 5 +---- 4 files changed, 14 insertions(+), 56 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 2dd20ff8..562a53af 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -767,7 +767,6 @@ data-identifier="${config['pms_identifier']}" data-ip="${config['pms_ip']}" data-port="${config['pms_port']}" - data-local="${int(not int(config['pms_is_remote']))}" data-ssl="${config['pms_ssl']}" data-is_cloud="${config['pms_is_cloud']}" data-label="${config['pms_name'] or 'Local'}" @@ -800,13 +799,6 @@

Connect to your Plex server using HTTPS if you have secure connections enabled.

-
- -

Check this if your Plex Server is not on the same local network as Tautulli.

-
@@ -2597,7 +2589,6 @@ $(document).ready(function() { return '
' + @@ -2611,7 +2602,6 @@ $(document).ready(function() { return '
' + @@ -2634,7 +2624,6 @@ $(document).ready(function() { var identifier = $(pms_ip_selected).data('identifier'); var ip = $(pms_ip_selected).data('ip'); var port = $(pms_ip_selected).data('port'); - var local = $(pms_ip_selected).data('local'); var ssl = $(pms_ip_selected).data('ssl'); var is_cloud = $(pms_ip_selected).data('is_cloud'); var value = $(pms_ip_selected).data('value'); @@ -2642,8 +2631,6 @@ $(document).ready(function() { $("#pms_identifier").val(identifier !== 'undefined' ? identifier : ''); $('#pms_ip').val(ip !== 'undefined' ? ip : value); $('#pms_port').val(port !== 'undefined' ? port : 32400); - $('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0)); - $('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0); $('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1)); $('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0); $('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0); @@ -2681,7 +2668,6 @@ $(document).ready(function() { var pms_port = $("#pms_port").val(); var pms_identifier = $("#pms_identifier").val(); var pms_ssl = $("#pms_ssl").val(); - var pms_is_remote = $("#pms_is_remote").val(); var pms_url_manual = $("#pms_url_manual").is(':checked') ? 1 : 0; if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) { @@ -2693,7 +2679,6 @@ $(document).ready(function() { hostname: pms_ip, port: pms_port, ssl: pms_ssl, - remote: pms_is_remote, manual: pms_url_manual, get_url: true, test_websocket: true diff --git a/data/interfaces/default/welcome.html b/data/interfaces/default/welcome.html index 39fc19b4..e3b4bc56 100644 --- a/data/interfaces/default/welcome.html +++ b/data/interfaces/default/welcome.html @@ -135,7 +135,6 @@ data-identifier="${config['pms_identifier']}" data-ip="${config['pms_ip']}" data-port="${config['pms_port']}" - data-local="${int(not int(config['pms_is_remote']))}" data-ssl="${config['pms_ssl']}" data-is_cloud="${config['pms_is_cloud']}" data-label="${config['pms_name'] or 'Local'}" @@ -159,14 +158,6 @@
-
-
- -
-
@@ -391,7 +382,6 @@ $(document).ready(function() { return '
' + @@ -405,7 +395,6 @@ $(document).ready(function() { return '
' + @@ -428,7 +417,6 @@ $(document).ready(function() { var identifier = $(pms_ip_selected).data('identifier'); var ip = $(pms_ip_selected).data('ip'); var port = $(pms_ip_selected).data('port'); - var local = $(pms_ip_selected).data('local'); var ssl = $(pms_ip_selected).data('ssl'); var is_cloud = $(pms_ip_selected).data('is_cloud'); var value = $(pms_ip_selected).data('value'); @@ -439,19 +427,15 @@ $(document).ready(function() { $("#pms_identifier").val(identifier !== 'undefined' ? identifier : ''); $('#pms_ip').val(ip !== 'undefined' ? ip : value); $('#pms_port').val(port !== 'undefined' ? port : 32400); - $('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0)); - $('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0); $('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1)); $('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0); $('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0); if (is_cloud === true) { $('#pms_port').prop('readonly', true); - $('#pms_is_remote_checkbox').prop('disabled', true); $('#pms_ssl_checkbox').prop('disabled', true); } else { $('#pms_port').prop('readonly', false); - $('#pms_is_remote_checkbox').prop('disabled', false); $('#pms_ssl_checkbox').prop('disabled', false); } }, @@ -488,7 +472,6 @@ $(document).ready(function() { var pms_port = $("#pms_port").val().trim(); var pms_identifier = $("#pms_identifier").val(); var pms_ssl = $("#pms_ssl").val(); - var pms_is_remote = $("#pms_is_remote").val(); if ((pms_ip !== '') || (pms_port !== '')) { $("#pms-verify-status").html('  Verifying server...'); $('#pms-verify-status').fadeIn('fast'); @@ -498,8 +481,7 @@ $(document).ready(function() { hostname: pms_ip, port: pms_port, identifier: pms_identifier, - ssl: pms_ssl, - remote: pms_is_remote + ssl: pms_ssl }, cache: true, async: true, diff --git a/plexpy/plextv.py b/plexpy/plextv.py index d45ce785..eed9d50e 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -39,7 +39,6 @@ def get_server_resources(return_presence=False, return_server=False, return_info 'pms_ip': plexpy.CONFIG.PMS_IP, 'pms_port': plexpy.CONFIG.PMS_PORT, 'pms_ssl': plexpy.CONFIG.PMS_SSL, - 'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE, 'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD, 'pms_url': plexpy.CONFIG.PMS_URL, 'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL, @@ -52,7 +51,7 @@ def get_server_resources(return_presence=False, return_server=False, return_info if kwargs: server.update(kwargs) - for k in ['pms_ssl', 'pms_is_remote', 'pms_is_cloud', 'pms_url_manual']: + for k in ['pms_ssl', 'pms_is_cloud', 'pms_url_manual']: server[k] = int(server[k]) if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']: @@ -88,30 +87,25 @@ def get_server_resources(return_presence=False, return_server=False, return_info # Only need to retrieve PMS_URL if using SSL if not server['pms_url_manual'] and server['pms_ssl']: if connections: - if server['pms_is_remote']: - # Get all remote connections - conns = [c for c in connections if - c['local'] == '0' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])] - else: - # Get all local connections - conns = [c for c in connections if - c['local'] == '1' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])] - - if conns: - # Get connection with matching address, otherwise return first connection - conn = next((c for c in conns if c['address'] == server['pms_ip'] - and c['port'] == str(server['pms_port'])), conns[0]) - server['pms_url'] = conn['uri'] - logger.info("Tautulli PlexTV :: Server URL retrieved.") + # Get connection with matching address, otherwise return first connection + connection = next( + (c for c in connections if c['address'] == server['pms_ip'] and c['port'] == str(server['pms_port'])), + connections[0] + ) + server['pms_url'] = connection['uri'] + server['pms_is_remote'] = int(connection['local'] == '0') + logger.info("Tautulli PlexTV :: Server URL retrieved.") # get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL if not server['pms_url']: server['pms_url'] = fallback_url + server['pms_is_remote'] = 0 logger.warn("Tautulli PlexTV :: Unable to retrieve server URLs. Using user-defined value without SSL.") - # Not using SSL, remote has no effect + # Not using SSL else: server['pms_url'] = fallback_url + server['pms_is_remote'] = 0 logger.info("Tautulli PlexTV :: Using user-defined URL.") if return_server: diff --git a/plexpy/webserve.py b/plexpy/webserve.py index ec98baf2..150bc18d 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -175,7 +175,6 @@ class WebInterface(object): "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER, "pms_ip": plexpy.CONFIG.PMS_IP, "pms_port": plexpy.CONFIG.PMS_PORT, - "pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE, "pms_ssl": plexpy.CONFIG.PMS_SSL, "pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD, "pms_name": helpers.pms_name(), @@ -3247,7 +3246,6 @@ class WebInterface(object): # If we change the SSL setting for PMS or PMS remote setting, make sure we grab the new url. if kwargs.get('pms_ssl') != str(plexpy.CONFIG.PMS_SSL) or \ - kwargs.get('pms_is_remote') != str(plexpy.CONFIG.PMS_IS_REMOTE) or \ kwargs.get('pms_url_manual') != plexpy.CONFIG.PMS_URL_MANUAL: server_changed = True @@ -4060,7 +4058,7 @@ class WebInterface(object): @cherrypy.tools.json_out() @requireAuth(member_of("admin")) @addtoapi() - def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, manual=0, + def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, manual=0, get_url=False, test_websocket=False, **kwargs): """ Get the PMS server identifier. @@ -4115,7 +4113,6 @@ class WebInterface(object): server = self.get_server_resources(pms_ip=hostname, pms_port=port, pms_ssl=ssl, - pms_is_remote=remote, pms_url_manual=manual, pms_identifier=identifier) result['url'] = server['pms_url'] From a3af8ed36210e49350c5233487ebf5b832190a39 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:07:11 -0700 Subject: [PATCH 014/127] Fix width of use secure connection checkbox in wizard --- data/interfaces/default/welcome.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/interfaces/default/welcome.html b/data/interfaces/default/welcome.html index e3b4bc56..baf1b57e 100644 --- a/data/interfaces/default/welcome.html +++ b/data/interfaces/default/welcome.html @@ -150,7 +150,7 @@
-
+