From 61c851cce5dcb786258c9c73438950069983b0ef Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:04:25 -0800 Subject: [PATCH 1/5] Add support for on-demand subtitle search --- plexapi/video.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/plexapi/video.py b/plexapi/video.py index e95b12ffb..ec006b356 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -146,6 +146,48 @@ def uploadSubtitles(self, filepath): self._server.query(url, self._server._session.post, data=subfile, params=params, headers=headers) return self + def searchSubtitles(self, language='en', hearingImpaired=0, forced=0): + """ Search for on-demand subtitles for the video. + See https://support.plex.tv/articles/subtitle-search/. + + Parameters: + language (str, optional): Language code (ISO 639-1) of the subtitles to search for. + Default 'en'. + hearingImpaired (int, optional): Search option for SDH subtitles. + Default 0. + (0 = Prefer non-SDH subtitles, 1 = Prefer SDH subtitles, + 2 = Only show SDH subtitles, 3 = Only show non-SDH subtitles) + forced (int, optional): Search option for forced subtitles. + Default 0. + (0 = Prefer non-forced subtitles, 1 = Prefer forced subtitles, + 2 = Only show forced subtitles, 3 = Only show non-forced subtitles) + + Returns: + List<:class:`~plexapi.media.SubtitleStream`>: List of SubtitleStream objects. + """ + params = { + 'language': language, + 'hearingImpaired': hearingImpaired, + 'forced': forced, + } + key = f'{self.key}/subtitles{utils.joinArgs(params)}' + return self.fetchItems(key) + + def downloadSubtitles(self, subtitleStream): + """ Download on-demand subtitles for the video. + See https://support.plex.tv/articles/subtitle-search/. + + Note: This method is asynchronous and returns immediately before subtitles are fully downloaded. + + Parameters: + subtitleStream (:class:`~plexapi.media.SubtitleStream`): + Subtitle object returned from :func:`~plexapi.video.Video.searchSubtitles`. + """ + key = f'{self.key}/subtitles' + params = {'key': subtitleStream.key} + self._server.query(key, self._server._session.put, params=params) + return self + def removeSubtitles(self, streamID=None, streamTitle=None): """ Remove Subtitle from movie's subtitles listing. From fd7b67f56f1da4a062ba666fce111b64a189ba84 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:38:54 -0800 Subject: [PATCH 2/5] Clean up uploadSubtitles and removeSubtitles methods --- plexapi/video.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/plexapi/video.py b/plexapi/video.py index ec006b356..a5dfda540 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -134,15 +134,20 @@ def subtitleStreams(self): return streams def uploadSubtitles(self, filepath): - """ Upload Subtitle file for video. """ + """ Upload a subtitle file for the video. + + Parameters: + filepath (str): Path to subtitle file. + """ url = f'{self.key}/subtitles' filename = os.path.basename(filepath) subFormat = os.path.splitext(filepath)[1][1:] + params = { + 'title': filename, + 'format': subFormat, + } + headers = {'Accept': 'text/plain, */*'} with open(filepath, 'rb') as subfile: - params = {'title': filename, - 'format': subFormat - } - headers = {'Accept': 'text/plain, */*'} self._server.query(url, self._server._session.post, data=subfile, params=params, headers=headers) return self @@ -188,15 +193,28 @@ def downloadSubtitles(self, subtitleStream): self._server.query(key, self._server._session.put, params=params) return self - def removeSubtitles(self, streamID=None, streamTitle=None): - """ Remove Subtitle from movie's subtitles listing. + def removeSubtitles(self, subtitleStream=None, streamID=None, streamTitle=None): + """ Remove an upload or downloaded subtitle from the video. + + Note: If the subtitle file is located inside video directory it will be deleted. + Files outside of video directory are not affected. + Embedded subtitles cannot be removed. - Note: If subtitle file is located inside video directory it will bbe deleted. - Files outside of video directory are not effected. + Parameters: + subtitleStream (:class:`~plexapi.media.SubtitleStream`, optional): Subtitle object to remove. + streamID (int, optional): ID of the subtitle stream to remove. + streamTitle (str, optional): Title of the subtitle stream to remove. """ - for stream in self.subtitleStreams(): - if streamID == stream.id or streamTitle == stream.title: - self._server.query(stream.key, self._server._session.delete) + if subtitleStream is None: + try: + subtitleStream = next( + stream for stream in self.subtitleStreams() + if streamID == stream.id or streamTitle == stream.title + ) + except StopIteration: + raise BadRequest(f"Subtitle stream with ID '{streamID}' or title '{streamTitle}' not found.") from None + + self._server.query(subtitleStream.key, self._server._session.delete) return self def optimize(self, title='', target='', deviceProfile='', videoQuality=None, From fb20ce0ce9f01987b39cf93791aa9339d9713292 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:17:19 -0800 Subject: [PATCH 3/5] Add test for on-demand subtitles Account is required for on-demand subtitles --- tests/test_video.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_video.py b/tests/test_video.py index 002dbd184..180155b1a 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -397,7 +397,6 @@ def test_video_Episode_subtitleStreams(episode): def test_video_Movie_upload_select_remove_subtitle(movie, subtitle): - filepath = os.path.realpath(subtitle.name) movie.uploadSubtitles(filepath) @@ -407,7 +406,6 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle): movie.subtitleStreams()[0].setSelected() movie.reload() - subtitleSelection = movie.subtitleStreams()[0] assert subtitleSelection.selected @@ -422,6 +420,22 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle): pass +def test_video_Movie_on_demand_subtitles(movie, account): + movie_subtitles = movie.subtitleStreams() + subtitles = movie.searchSubtitles() + assert subtitles != [] + + subtitle = subtitles[0] + + movie.downloadSubtitles(subtitle) + utils.wait_until(lambda: len(movie.reload().subtitleStreams()) > len(movie_subtitles)) + subtitle_sourceKeys = {stream.sourceKey: stream for stream in movie.subtitleStreams()} + assert subtitle.sourceKey in subtitle_sourceKeys + + movie.removeSubtitles(subtitleStream=subtitle_sourceKeys[subtitle.sourceKey]).reload() + assert subtitle.sourceKey not in [stream.sourceKey for stream in movie.subtitleStreams()] + + def test_video_Movie_match(movies): sectionAgent = movies.agent sectionAgents = [agent.identifier for agent in movies.agents() if agent.shortIdentifier != 'none'] From 880536cf5f5785998a901bb46e8fae6b573616a0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 25 Nov 2023 16:26:22 -0800 Subject: [PATCH 4/5] Add hearingImpaired and perfectMatch to SubtitleStream attributes --- plexapi/media.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plexapi/media.py b/plexapi/media.py index 10c97e200..e7baae52d 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -444,6 +444,8 @@ class SubtitleStream(MediaPartStream): forced (bool): True if this is a forced subtitle. format (str): The format of the subtitle stream (ex: srt). headerCompression (str): The header compression of the subtitle stream. + hearingImpaired (bool): True if this is a hearing impaired (SDH) subtitle. + perfectMatch (bool): True if the on-demand subtitle is a perfect match. providerTitle (str): The provider title where the on-demand subtitle is downloaded from. score (int): The match score of the on-demand subtitle. sourceKey (str): The source key of the on-demand subtitle. @@ -460,6 +462,8 @@ def _loadData(self, data): self.forced = utils.cast(bool, data.attrib.get('forced', '0')) self.format = data.attrib.get('format') self.headerCompression = data.attrib.get('headerCompression') + self.hearingImpaired = utils.cast(bool, data.attrib.get('hearingImpaired', '0')) + self.perfectMatch = utils.cast(bool, data.attrib.get('perfectMatch')) self.providerTitle = data.attrib.get('providerTitle') self.score = utils.cast(int, data.attrib.get('score')) self.sourceKey = data.attrib.get('sourceKey') From cfec9e151b93a13ee818b89340acee6b02f76ab2 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:43:50 -0800 Subject: [PATCH 5/5] Update subtitle score doc string The score is the OpenSubtitles download count Ref: https://forums.plex.tv/t/subtitle-search-update-opensubtitles/862746 --- plexapi/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/media.py b/plexapi/media.py index e7baae52d..4ea5c28cb 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -447,7 +447,7 @@ class SubtitleStream(MediaPartStream): hearingImpaired (bool): True if this is a hearing impaired (SDH) subtitle. perfectMatch (bool): True if the on-demand subtitle is a perfect match. providerTitle (str): The provider title where the on-demand subtitle is downloaded from. - score (int): The match score of the on-demand subtitle. + score (int): The match score (download count) of the on-demand subtitle. sourceKey (str): The source key of the on-demand subtitle. transient (str): Unknown. userID (int): The user id of the user that downloaded the on-demand subtitle.