Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,10 @@ 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.
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.
Expand All @@ -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')
Expand Down
84 changes: 72 additions & 12 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,27 +134,87 @@ 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

def removeSubtitles(self, streamID=None, streamTitle=None):
""" Remove Subtitle from movie's subtitles listing.
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/.

Note: If subtitle file is located inside video directory it will bbe deleted.
Files outside of video directory are not effected.
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, 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.

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,
Expand Down
18 changes: 16 additions & 2 deletions tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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']
Expand Down