From 63dc1507d28a41beecec5b159b7f93d53ff69fcb Mon Sep 17 00:00:00 2001 From: Michael Shepanski Date: Sun, 13 Aug 2017 01:50:40 -0400 Subject: [PATCH] Add plex-download.py tool; Added new utility to request user/pass from user, config, or env for use when creating cmd line tools --- plexapi/base.py | 2 +- plexapi/utils.py | 51 ++++++++++++++++-------- tools/plex-download.py | 67 +++++++++++++++++++++++++++++++ tools/plex-listtokens.py | 85 ++++++++++++++++++++++++---------------- 4 files changed, 155 insertions(+), 50 deletions(-) create mode 100755 tools/plex-download.py diff --git a/plexapi/base.py b/plexapi/base.py index a48c7aa3..173b1ed0 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -92,7 +92,7 @@ class PlexObject(object): the first item in the result set is returned. Parameters: - key (str or int): Path in Plex to fetch items from. If an int is passed + ekey (str or int): Path in Plex to fetch items from. If an int is passed in, the key will be translated to /library/metadata/. This allows fetching an item only knowing its key-id. cls (:class:`~plexapi.base.PlexObject`): If you know the class of the diff --git a/plexapi/utils.py b/plexapi/utils.py index 2e3de11c..12232ed6 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -import logging -import os -import re -import requests -import time -import zipfile +import logging, os, re, requests, time, zipfile from datetime import datetime +from getpass import getpass from threading import Thread from plexapi import compat from plexapi.exceptions import NotFound @@ -281,20 +277,43 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024, un def tag_helper(tag, items, locked=True, remove=False): - """Simple tag helper for editing a object.""" + """ Simple tag helper for editing a object. """ if not isinstance(items, list): items = [items] - - d = {} + data = {} if not remove: for i, item in enumerate(items): - tag_name = '%s[%s].tag.tag' % (tag, i) - d[tag_name] = item - + tagname = '%s[%s].tag.tag' % (tag, i) + data[tagname] = item if remove: - tag_name = '%s[].tag.tag-' % tag - d[tag_name] = ','.join(items) + tagname = '%s[].tag.tag-' % tag + data[tagname] = ','.join(items) + data['%s.locked' % tag] = 1 if locked else 0 + return data - d['%s.locked' % tag] = 1 if locked else 0 - return d +def getMyPlexAccount(opts=None): + """ Helper function tries to get a MyPlex Account instance by checking + the the following locations for a username and password. This is + useful to create user-friendly command line tools. + 1. command-line options (opts). + 2. environment variables and config.ini + 3. Prompt on the command line. + """ + from plexapi import CONFIG + from plexapi.myplex import MyPlexAccount + # 1. Check command-line options + if opts and opts.username and opts.password: + print('Authenticating with Plex.tv as %s..' % opts.username) + return MyPlexAccount(opts.username, opts.password) + # 2. Check Plexconfig (environment variables and config.ini) + config_username = CONFIG.get('auth.myplex_username') + config_password = CONFIG.get('auth.myplex_password') + if config_username and config_password: + print('Authenticating with Plex.tv as %s..' % config_username) + return MyPlexAccount(config_username, config_password) + # 3. Prompt for username and password on the command line + username = input('What is your plex.tv username: ') + password = getpass('What is your plex.tv password: ') + print('Authenticating with Plex.tv as %s..' % username) + return MyPlexAccount(username, password) diff --git a/tools/plex-download.py b/tools/plex-download.py new file mode 100755 index 00000000..19f7a36e --- /dev/null +++ b/tools/plex-download.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Allows downloading a Plex media item from a local or shared library. You +may specify the item by the PlexWeb url (everything after !) or by +manually searching the items from the command line wizard. + +Original contribution by lad1337. +""" +import argparse, re +from plexapi import utils +from plexapi.compat import unquote +from plexapi.video import Episode, Movie, Show + + +def choose(msg, items, attr): + print() + for index, i in enumerate(items): + name = attr(i) if callable(attr) else getattr(i, attr) + print(' %s: %s' % (index, name)) + number = int(input('\n%s: ' % msg)) + return items[number] + + +def search_for_item(url=None): + if url: return get_item_from_url(opts.url) + server = choose('Choose a Server', account.resources(), 'name').connect() + query = input('What are you looking for?: ') + item = choose('Choose result', server.search(query), lambda x: repr(x)) + if isinstance(item, Show): + item = choose('Choose episode', item.episodes(), lambda x: x._prettyfilename()) + if not isinstance(item, (Movie, Episode)): + raise SystemExit('Unable to download %s' % item.__class__.__name__) + return item + + +def get_item_from_url(url): + # Parse the ClientID and Key from the URL + clientid = re.findall('[a-f0-9]{40}', url) + key = re.findall('key=(.*?)(&.*)?$', url) + if not clientid or not key: + raise SystemExit('Cannot parse URL: %s' % url) + clientid = clientid[0] + key = unquote(key[0][0]) + # Connect to the server and fetch the item + servers = [r for r in account.resources() if r.clientIdentifier == clientid] + if len(servers) != 1: + raise SystemExit('Unknown or ambiguous client id: %s' % clientid) + server = servers[0].connect() + return server.fetchItem(key) + + +if __name__ == '__main__': + # Command line parser + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--username', help='Your Plex username') + parser.add_argument('--password', help='Your Plex password') + parser.add_argument('--url', default=None, help='Download from URL (only paste after !)') + opts = parser.parse_args() + # Search item to download + account = utils.getMyPlexAccount(opts) + item = search_for_item(opts.url) + # Download the item + print("Downloading '%s' from %s.." % (item._prettyfilename(), item._server.friendlyName)) + filepaths = item.download('./') + for filepath in filepaths: + print(' %s' % filepath) diff --git a/tools/plex-listtokens.py b/tools/plex-listtokens.py index ba8da6f1..6f0e6162 100755 --- a/tools/plex-listtokens.py +++ b/tools/plex-listtokens.py @@ -8,45 +8,45 @@ and password. Alternatively, if you do not wish to enter your login information below, you can retrieve the same information from plex.tv at the URL: https://plex.tv/api/resources?includeHttps=1 """ -from getpass import getpass +import argparse from plexapi import utils from plexapi.exceptions import BadRequest -from plexapi.myplex import MyPlexAccount, _connect +from plexapi.myplex import _connect from plexapi.server import PlexServer -FORMAT = ' %-17s %-25s %-20s %s' -FORMAT2 = ' %-17s %-25s %-20s %-30s (%s)' SERVER = 'Plex Media Server' +FORMAT = '%-8s %-6s %-17s %-25s %-20s %s (%s)' def _list_resources(account, servers): - print('\nHTTPS Resources:') - resources = MyPlexAccount(username, password).resources() - for r in resources: - if r.accessToken: - for connection in r.connections: - print(FORMAT % (r.product, r.name, r.accessToken, connection.uri)) - servers[connection.uri] = r.accessToken - print('\nDirect Resources:') - for r in resources: - if r.accessToken: - for connection in r.connections: - print(FORMAT % (r.product, r.name, r.accessToken, connection.httpuri)) - servers[connection.httpuri] = r.accessToken + items = [] + print('Finding Plex resources..') + resources = account.resources() + for r in [r for r in resources if r.accessToken]: + for connection in r.connections: + local = 'Local' if connection.local else 'Remote' + extras = [r.provides] + items.append(FORMAT % ('Resource', local, r.product, r.name, r.accessToken, connection.uri, ','.join(extras))) + items.append(FORMAT % ('Resource', local, r.product, r.name, r.accessToken, connection.httpuri, ','.join(extras))) + servers[connection.httpuri] = r.accessToken + servers[connection.uri] = r.accessToken + return items def _list_devices(account, servers): - print('\nDevices:') - for d in MyPlexAccount(username, password).devices(): - if d.token: - for conn in d.connections: - print(FORMAT % (d.product, d.name, d.token, conn)) - servers[conn] = d.token + items = [] + print('Finding Plex devices..') + for d in [d for d in account.devices() if d.token]: + for connection in d.connections: + extras = [d.provides] + items.append(FORMAT % ('Device', '--', d.product, d.name, d.token, connection, ','.join(extras))) + servers[connection] = d.token + return items def _test_servers(servers): - seen = set() - print('\nServer Clients:') + items, seen = [], set() + print('Finding Plex clients..') listargs = [[PlexServer, s, t, 5] for s,t in servers.items()] results = utils.threaded(_connect, listargs) for url, token, plex, runtime in results: @@ -54,19 +54,38 @@ def _test_servers(servers): if plex and clients: for c in plex.clients(): if c._baseurl not in seen: - print(FORMAT2 % (c.product, c.title, token, c._baseurl, plex.friendlyName)) + extras = [plex.friendlyName] + c.protocolCapabilities + items.append(FORMAT % ('Client', '--', c.product, c.title, token, c._baseurl, ','.join(extras))) seen.add(c._baseurl) + return items + + +def _print_items(items, _filter=None): + if _filter: + print('Displaying items matching filter: %s' % _filter) + print() + for item in items: + filtered_out = False + for f in _filter.split(): + if f.lower() not in item.lower(): + filtered_out = True + if not filtered_out: + print(item) + print() if __name__ == '__main__': - print(__doc__) - username = input('What is your plex.tv username: ') - password = getpass('What is your plex.tv password: ') + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--username', help='Your Plex username') + parser.add_argument('--password', help='Your Plex password') + parser.add_argument('--filter', default='', help='Only display items containing specified filter') + opts = parser.parse_args() try: servers = {} - account = MyPlexAccount(username, password) - _list_resources(account, servers) - _list_devices(account, servers) - _test_servers(servers) + account = utils.getMyPlexAccount(opts) + items = _list_resources(account, servers) + items += _list_devices(account, servers) + items += _test_servers(servers) + _print_items(items, opts.filter) except BadRequest as err: print('Unable to login to plex.tv: %s' % err)