Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup download methods #847

Merged
merged 5 commits into from
Nov 20, 2021
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
41 changes: 18 additions & 23 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
from urllib.parse import quote_plus

from plexapi import library, media, utils
Expand Down Expand Up @@ -205,23 +206,20 @@ def get(self, title=None, album=None, track=None):
""" Alias of :func:`~plexapi.audio.Artist.track`. """
return self.track(title, album, track)

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Downloads all tracks for the artist to the specified location.
def download(self, savepath=None, keep_original_name=False, subfolders=False, **kwargs):
""" Download all tracks from the artist. See :func:`~plexapi.base.Playable.download` for details.

Parameters:
savepath (str): Title of the track to return.
keep_original_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate tracks in to album folders.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
for album in self.albums():
for track in album.tracks():
filepaths += track.download(savepath, keep_original_name, **kwargs)
for track in self.tracks():
_savepath = os.path.join(savepath, track.parentTitle) if subfolders else savepath
filepaths += track.download(_savepath, keep_original_name, **kwargs)
return filepaths


Expand Down Expand Up @@ -314,17 +312,13 @@ def artist(self):
return self.fetchItem(self.parentKey)

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Downloads all tracks for the artist to the specified location.
""" Download all tracks from the album. See :func:`~plexapi.base.Playable.download` for details.

Parameters:
savepath (str): Title of the track to return.
keep_original_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
for track in self.tracks():
Expand Down Expand Up @@ -398,7 +392,8 @@ def _loadData(self, data):

def _prettyfilename(self):
""" Returns a filename for use in download. """
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
return '%s - %s - %s - %s' % (
self.grandparentTitle, self.parentTitle, str(self.trackNumber).zfill(2), self.title)

def album(self):
""" Return the track's :class:`~plexapi.audio.Album`. """
Expand Down
52 changes: 34 additions & 18 deletions plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,34 +681,50 @@ def play(self, client):
client.playMedia(self)

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Downloads this items media to the specified location. Returns a list of
""" Downloads the media item to the specified location. Returns a list of
filepaths that have been saved to disk.

Parameters:
savepath (str): Title of the track to return.
keep_original_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Artist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated. See filenames below.
**kwargs (dict): Additional options passed into :func:`~plexapi.audio.Track.getStreamURL`
to download a transcoded stream, otherwise the media item will be downloaded
as-is and saved to disk.

**Filenames**

* Movie: ``<title> (<year>)``
* Episode: ``<show title> - s00e00 - <episode title>``
* Track: ``<artist title> - <album title> - 00 - <track title>``
* Photo: ``<photoalbum title> - <photo/clip title>`` or ``<photo/clip title>``
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
filename = location.file
if keep_original_name is False:
filename = '%s.%s' % (self._prettyfilename(), location.container)
# So this seems to be a alot slower but allows transcode.
parts = [i for i in self.iterParts() if i]

for part in parts:
if not keep_original_name:
filename = utils.cleanFilename('%s.%s' % (self._prettyfilename(), part.container))
else:
filename = part.file

if kwargs:
# So this seems to be a alot slower but allows transcode.
download_url = self.getStreamURL(**kwargs)
else:
download_url = self._server.url('%s?download=1' % location.key)
filepath = utils.download(download_url, self._server._token, filename=filename,
savepath=savepath, session=self._server._session)
download_url = self._server.url('%s?download=1' % part.key)

filepath = utils.download(
download_url,
self._server._token,
filename=filename,
savepath=savepath,
session=self._server._session
)

if filepath:
filepaths.append(filepath)

return filepaths

def stop(self, reason=''):
Expand Down
69 changes: 17 additions & 52 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
from urllib.parse import quote_plus

from plexapi import media, utils, video
Expand Down Expand Up @@ -107,34 +108,21 @@ def get(self, title):
""" Alias to :func:`~plexapi.photo.Photoalbum.photo`. """
return self.episode(title)

def iterParts(self):
""" Iterates over the parts of the media item. """
for album in self.albums():
for photo in album.photos():
for part in photo.iterParts():
yield part

def download(self, savepath=None, keep_original_name=False, showstatus=False):
""" Download photo files to specified directory.
def download(self, savepath=None, keep_original_name=False, subfolders=False):
""" Download all photos and clips from the photo ablum. See :func:`~plexapi.base.Playable.download` for details.

Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
showstatus(bool): Display a progressbar.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate photos/clips in to photo album folders.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
name = location.file
if not keep_original_name:
title = self.title.replace(' ', '.')
name = '%s.%s' % (title, location.container)
url = self._server.url('%s?download=1' % location.key)
filepath = utils.download(url, self._server._token, filename=name, showstatus=showstatus,
savepath=savepath, session=self._server._session)
if filepath:
filepaths.append(filepath)
for album in self.albums():
_savepath = os.path.join(savepath, album.title) if subfolders else savepath
filepaths += album.download(_savepath, keep_original_name)
for photo in self.photos() + self.clips():
filepaths += photo.download(savepath, keep_original_name)
return filepaths

def _getWebURL(self, base=None):
Expand Down Expand Up @@ -218,6 +206,12 @@ def _loadData(self, data):
self.userRating = utils.cast(float, data.attrib.get('userRating'))
self.year = utils.cast(int, data.attrib.get('year'))

def _prettyfilename(self):
""" Returns a filename for use in download. """
if self.parentTitle:
return '%s - %s' % (self.parentTitle, self.title)
return self.title

def photoalbum(self):
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
return self.fetchItem(self.parentKey)
Expand All @@ -241,12 +235,6 @@ def locations(self):
"""
return [part.file for item in self.media for part in item.parts if part]

def iterParts(self):
""" Iterates over the parts of the media item. """
for item in self.media:
for part in item.parts:
yield part

def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
""" Add current photo as sync item for specified device.
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
Expand Down Expand Up @@ -283,29 +271,6 @@ def sync(self, resolution, client=None, clientId=None, limit=None, title=None):

return myplex.sync(sync_item, client=client, clientId=clientId)

def download(self, savepath=None, keep_original_name=False, showstatus=False):
""" Download photo files to specified directory.

Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
showstatus(bool): Display a progressbar.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
name = location.file
if not keep_original_name:
title = self.title.replace(' ', '.')
name = '%s.%s' % (title, location.container)
url = self._server.url('%s?download=1' % location.key)
filepath = utils.download(url, self._server._token, filename=name, showstatus=showstatus,
savepath=savepath, session=self._server._session)
if filepath:
filepaths.append(filepath)
return filepaths

def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey, legacy=1)
9 changes: 9 additions & 0 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import logging
import os
import re
import string
import time
import unicodedata
import warnings
import zipfile
from datetime import datetime
Expand Down Expand Up @@ -251,6 +253,13 @@ def toList(value, itemcast=None, delim=','):
return [itemcast(item) for item in value.split(delim) if item != '']


def cleanFilename(filename, replace='_'):
whitelist = "-_.()[] {}{}".format(string.ascii_letters, string.digits)
cleaned_filename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode()
cleaned_filename = ''.join(c if c in whitelist else replace for c in cleaned_filename)
return cleaned_filename


def downloadSessionImages(server, filename=None, height=150, width=150,
opacity=100, saturation=100): # pragma: no cover
""" Helper to download a bif image or thumb.url from plex.server.sessions.
Expand Down
54 changes: 16 additions & 38 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ def hasPreviewThumbnails(self):
return any(part.hasPreviewThumbnails for media in self.media for part in media.parts)

def _prettyfilename(self):
# This is just for compat.
return self.title
""" Returns a filename for use in download. """
return '%s (%s)' % (self.title, self.year)

def reviews(self):
""" Returns a list of :class:`~plexapi.media.Review` objects. """
Expand All @@ -375,32 +375,6 @@ def hubs(self):
data = self._server.query(self._details_key)
return self.findItems(data, library.Hub, rtag='Related')

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download video files to specified directory.

Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
name = location.file
if not keep_original_name:
title = self.title.replace(' ', '.')
name = '%s.%s' % (title, location.container)
if kwargs is not None:
url = self.getStreamURL(**kwargs)
else:
self._server.url('%s?download=1' % location.key)
filepath = utils.download(url, self._server._token, filename=name,
savepath=savepath, session=self._server._session)
if filepath:
filepaths.append(filepath)
return filepaths


@utils.registerPlexObject
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
Expand Down Expand Up @@ -582,18 +556,20 @@ def unwatched(self):
""" Returns list of unwatched :class:`~plexapi.video.Episode` objects. """
return self.episodes(viewCount=0)

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download video files to specified directory.
def download(self, savepath=None, keep_original_name=False, subfolders=False, **kwargs):
""" Download all episodes from the show. See :func:`~plexapi.base.Playable.download` for details.

Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate episodes in to season folders.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
for episode in self.episodes():
filepaths += episode.download(savepath, keep_original_name, **kwargs)
_savepath = os.path.join(savepath, 'Season %s' % str(episode.seasonNumber).zfill(2)) if subfolders else savepath
filepaths += episode.download(_savepath, keep_original_name, **kwargs)
return filepaths


Expand Down Expand Up @@ -714,12 +690,12 @@ def unwatched(self):
return self.episodes(viewCount=0)

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download video files to specified directory.
""" Download all episodes from the season. See :func:`~plexapi.base.Playable.download` for details.

Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original file name otherwise
a friendlier is generated.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
"""
filepaths = []
Expand Down Expand Up @@ -839,8 +815,8 @@ def __repr__(self):
] if p])

def _prettyfilename(self):
""" Returns a human friendly filename. """
return '%s.%s' % (self.grandparentTitle.replace(' ', '.'), self.seasonEpisode)
""" Returns a filename for use in download. """
return '%s - %s - %s' % (self.grandparentTitle, self.seasonEpisode, self.title)

@property
def actors(self):
Expand Down Expand Up @@ -953,6 +929,7 @@ def locations(self):
return [part.file for part in self.iterParts() if part]

def _prettyfilename(self):
""" Returns a filename for use in download. """
return self.title


Expand All @@ -968,4 +945,5 @@ def _loadData(self, data):
self.librarySectionTitle = parent.librarySectionTitle

def _prettyfilename(self):
""" Returns a filename for use in download. """
return '%s (%s)' % (self.title, self.subtype)
Loading