Skip to content

Commit 3526961

Browse files
committedDec 29, 2014
Move from Bitbucket
0 parents  commit 3526961

22 files changed

+1751
-0
lines changed
 

‎.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
syntax: glob
2+
*.db
3+
*.log
4+
*.pyc
5+
*.sublime-*
6+
*__pycache__*
7+
dist
8+
build
9+
*.egg-info
10+
.idea/

‎AUTHORS.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Primary Authors:
2+
* Michael Shepanski
3+
4+
Thanks to Contributors:
5+
* Nate Mara (Timeline)
6+
* Goni Zahavy (Sync, Media Parts)

‎LICENSE.txt

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (c) 2010, Michael Shepanski
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification,
5+
are permitted provided that the following conditions are met:
6+
7+
* Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright notice,
10+
this list of conditions and the following disclaimer in the documentation
11+
and/or other materials provided with the distribution.
12+
* Neither the name conky-pkmeter nor the names of its contributors
13+
may be used to endorse or promote products derived from this software without
14+
specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

‎MANIFEST.in

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include README.md
2+
include requirements.pip

‎README.md

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
## PlexAPI ##
2+
Python bindings for the Plex API.
3+
4+
* Navigate local or remote shared libraries.
5+
* Mark shows watched or unwatched.
6+
* Request rescan, analyze, empty trash.
7+
* Play media on connected clients.
8+
* Plex Sync Support.
9+
10+
Planned features:
11+
12+
* Create and maintain playlists.
13+
* List active sessions.
14+
* Play trailers and extras.
15+
* Provide useful utility scripts.
16+
* Better support for Music and Photos?
17+
18+
#### Install ###
19+
20+
pip install plexapi
21+
22+
#### Getting a PlexServer Instance ####
23+
24+
There are two types of authentication. If running the PlexAPI on the same
25+
network as the Plex Server (and you are not using Plex Users), you can
26+
authenticate without a username and password. Getting a PlexServer
27+
instance is as easy as the following:
28+
29+
from plexapi.server import PlexServer
30+
plex = PlexServer() # Defaults to localhost:32400
31+
32+
If you are running on a separate network or using Plex Users you need to log
33+
into MyPlex to get a PlexServer instance. An example of this is below. NOTE:
34+
Servername below is the name of the server (not the hostname and port). If
35+
logged into Plex Web you can see the server name in the top left above your
36+
available libraries.
37+
38+
from plexapi.myplex import MyPlexUser
39+
user = MyPlexUser('<USERNAME>', '<PASSWORD>')
40+
plex = user.getServer('<SERVERNAME>').connect()
41+
42+
#### Usage Examples ####
43+
44+
# Example 1: List all unwatched content in library.
45+
for section in plex.library.sections():
46+
print 'Unwatched content in %s:' % section.title
47+
for video in section.unwatched():
48+
print ' %s' % video.title
49+
50+
# Example 2: Mark all Conan episodes watched.
51+
plex.library.get('Conan (2010)').markWatched()
52+
53+
# Example 3: List all Clients connected to the Server.
54+
for client in plex.clients():
55+
print client.name
56+
57+
# Example 4: Play the Movie Avatar on my iPhone.
58+
avatar = plex.library.section('Movies').get('Avatar')
59+
client = plex.client("Michael's iPhone")
60+
client.playMedia(avatar)
61+
62+
# Example 5: List all content with the word 'Game' in the title.
63+
for video in plex.search('Game'):
64+
print '%s (%s)' % (video.title, video.TYPE)
65+
66+
# Example 6: List all movies directed by the same person as Jurassic Park.
67+
jurassic_park = plex.library.section('Movies').get('Jurassic Park')
68+
director = jurassic_park.directors[0]
69+
for movie in director.related():
70+
print movie.title
71+
72+
# Example 7: List files for the latest episode of Friends.
73+
the_last_one = plex.library.get('Friends').episodes()[-1]
74+
for part in the_last_one.iter_parts():
75+
print part.file
76+
77+
#### FAQs ####
78+
79+
**Q. Why are you using camelCase and not following PEP8 guidelines?**
80+
81+
A. This API reads XML documents provided by MyPlex and the Plex Server.
82+
We decided to conform to their style so that the API variable names directly
83+
match with the provided XML documents.

‎examples/__init__.py

Whitespace-only changes.

‎examples/examples.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
PlexAPI Examples
3+
4+
As of Plex version 0.9.11 I noticed that you must be logged in
5+
to browse even the plex server locatewd at localhost. You can
6+
run this example suite with the following command:
7+
8+
>> python examples.py -u <USERNAME> -p <PASSWORD> -s <SERVERNAME>
9+
"""
10+
import argparse, sys
11+
from os.path import dirname, abspath
12+
sys.path.append(dirname(dirname(abspath(__file__))))
13+
from utils import fetch_server, iter_tests
14+
15+
16+
def example_001_list_all_unwatched_content(plex):
17+
""" Example 1: List all unwatched content in library """
18+
for section in plex.library.sections():
19+
print 'Unwatched content in %s:' % section.title
20+
for video in section.unwatched():
21+
print ' %s' % video.title
22+
23+
24+
def example_002_mark_all_conan_episodes_watched(plex):
25+
""" Example 2: Mark all Conan episodes watched. """
26+
plex.library.get('Conan (2010)').markWatched()
27+
28+
29+
def example_003_list_all_clients(plex):
30+
""" Example 3: List all Clients connected to the Server. """
31+
for client in plex.clients():
32+
print client.name
33+
34+
35+
def example_004_play_avatar_on_iphone(plex):
36+
""" Example 4: Play the Movie Avatar on my iPhone. """
37+
avatar = plex.library.section('Movies').get('Avatar')
38+
client = plex.client("Michael's iPhone")
39+
client.playMedia(avatar)
40+
41+
42+
def example_005_search(plex):
43+
""" Example 5: List all content with the word 'Game' in the title. """
44+
for video in plex.search('Game'):
45+
print '%s (%s)' % (video.title, video.TYPE)
46+
47+
48+
def example_006_follow_the_talent(plex):
49+
""" Example 6: List all movies directed by the same person as Jurassic Park. """
50+
jurassic_park = plex.library.section('Movies').get('Jurassic Park')
51+
director = jurassic_park.directors[0]
52+
for movie in director.related():
53+
print movie.title
54+
55+
56+
def example_007_list_files(plex):
57+
""" Example 7: List files for the latest episode of Friends. """
58+
the_last_one = plex.library.get('Friends').episodes()[-1]
59+
for part in the_last_one.iter_parts():
60+
print part.file
61+
62+
63+
if __name__ == '__main__':
64+
parser = argparse.ArgumentParser(description='Run PlexAPI examples.')
65+
parser.add_argument('-s', '--server', help='Name of the Plex server (requires user/pass).')
66+
parser.add_argument('-u', '--username', help='Username for the Plex server.')
67+
parser.add_argument('-p', '--password', help='Password for the Plex server.')
68+
parser.add_argument('-n', '--name', help='Only run tests containing this string. Leave blank to run all examples.')
69+
args = parser.parse_args()
70+
plex = fetch_server(args)
71+
for example in iter_tests(__name__, args):
72+
example(plex)
73+

‎examples/tests.py

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"""
2+
Test Library Functions
3+
4+
As of Plex version 0.9.11 I noticed that you must be logged in
5+
to browse even the plex server locatewd at localhost. You can
6+
run this test suite with the following command:
7+
8+
>> python tests.py -u <USERNAME> -p <PASSWORD> -s <SERVERNAME>
9+
"""
10+
import argparse, sys, time
11+
from os.path import dirname, abspath
12+
sys.path.append(dirname(dirname(abspath(__file__))))
13+
from utils import log, run_tests
14+
15+
SHOW_SECTION = 'TV Shows'
16+
SHOW_TITLE = 'Game of Thrones'
17+
SHOW_SEASON = 'Season 1'
18+
SHOW_EPISODE = 'Winter Is Coming'
19+
MOVIE_SECTION = 'Movies'
20+
MOVIE_TITLE = 'Jurassic Park'
21+
PLEX_CLIENT = "Michael's iPhone"
22+
23+
24+
def test_001_server(plex):
25+
log(2, 'Username: %s' % plex.myPlexUsername)
26+
log(2, 'Platform: %s' % plex.platform)
27+
log(2, 'Version: %s' % plex.version)
28+
assert plex.myPlexUsername is not None, 'Unknown username.'
29+
assert plex.platform is not None, 'Unknown platform.'
30+
assert plex.version is not None, 'Unknown version.'
31+
32+
33+
def test_002_list_sections(plex):
34+
sections = [s.title for s in plex.library.sections()]
35+
log(2, 'Sections: %s' % sections)
36+
assert SHOW_SECTION in sections, '%s not a library section.' % SHOW_SECTION
37+
assert MOVIE_SECTION in sections, '%s not a library section.' % MOVIE_SECTION
38+
plex.library.section(SHOW_SECTION)
39+
plex.library.section(MOVIE_SECTION)
40+
41+
42+
def test_003_search_show(plex):
43+
result_server = plex.search(SHOW_TITLE)
44+
result_library = plex.library.search(SHOW_TITLE)
45+
result_shows = plex.library.section(SHOW_SECTION).search(SHOW_TITLE)
46+
result_movies = plex.library.section(MOVIE_SECTION).search(SHOW_TITLE)
47+
log(2, 'Searching for: %s' % SHOW_TITLE)
48+
log(4, 'Result Server: %s' % result_server)
49+
log(4, 'Result Library: %s' % result_library)
50+
log(4, 'Result Shows: %s' % result_shows)
51+
log(4, 'Result Movies: %s' % result_movies)
52+
assert result_server, 'Show not found.'
53+
assert result_server == result_library == result_shows, 'Show searches not consistent.'
54+
assert not result_movies, 'Movie search returned show title.'
55+
56+
57+
def test_004_search_movie(plex):
58+
result_server = plex.search(MOVIE_TITLE)
59+
result_library = plex.library.search(MOVIE_TITLE)
60+
result_shows = plex.library.section(SHOW_SECTION).search(MOVIE_TITLE)
61+
result_movies = plex.library.section(MOVIE_SECTION).search(MOVIE_TITLE)
62+
log(2, 'Searching for: %s' % MOVIE_TITLE)
63+
log(4, 'Result Server: %s' % result_server)
64+
log(4, 'Result Library: %s' % result_library)
65+
log(4, 'Result Shows: %s' % result_shows)
66+
log(4, 'Result Movies: %s' % result_movies)
67+
assert result_server, 'Movie not found.'
68+
assert result_server == result_library == result_movies, 'Movie searches not consistent.'
69+
assert not result_shows, 'Show search returned show title.'
70+
71+
72+
def test_005_navigate_to_show(plex):
73+
result_library = plex.library.get(SHOW_TITLE)
74+
result_shows = plex.library.section(SHOW_SECTION).get(SHOW_TITLE)
75+
try:
76+
result_movies = plex.library.section(MOVIE_SECTION).get(SHOW_TITLE)
77+
except:
78+
result_movies = None
79+
log(2, 'Navigating to: %s' % SHOW_TITLE)
80+
log(4, 'Result Library: %s' % result_library)
81+
log(4, 'Result Shows: %s' % result_shows)
82+
log(4, 'Result Movies: %s' % result_movies)
83+
assert result_library == result_shows, 'Show navigation not consistent.'
84+
assert not result_movies, 'Movie navigation returned show title.'
85+
86+
87+
def test_006_navigate_to_movie(plex):
88+
result_library = plex.library.get(MOVIE_TITLE)
89+
result_movies = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
90+
try:
91+
result_shows = plex.library.section(SHOW_SECTION).get(MOVIE_TITLE)
92+
except:
93+
result_shows = None
94+
log(2, 'Navigating to: %s' % MOVIE_TITLE)
95+
log(4, 'Result Library: %s' % result_library)
96+
log(4, 'Result Shows: %s' % result_shows)
97+
log(4, 'Result Movies: %s' % result_movies)
98+
assert result_library == result_movies, 'Movie navigation not consistent.'
99+
assert not result_shows, 'Show navigation returned show title.'
100+
101+
102+
def test_007_navigate_around_show(plex):
103+
show = plex.library.get(SHOW_TITLE)
104+
seasons = show.seasons()
105+
season = show.season(SHOW_SEASON)
106+
episodes = show.episodes()
107+
episode = show.episode(SHOW_EPISODE)
108+
log(2, 'Navigating around show: %s' % show)
109+
log(4, 'Seasons: %s...' % seasons[:3])
110+
log(4, 'Season: %s' % season)
111+
log(4, 'Episodes: %s...' % episodes[:3])
112+
log(4, 'Episode: %s' % episode)
113+
assert SHOW_SEASON in [s.title for s in seasons], 'Unable to get season: %s' % SHOW_SEASON
114+
assert SHOW_EPISODE in [e.title for e in episodes], 'Unable to get episode: %s' % SHOW_EPISODE
115+
assert season.show() == show, 'season.show() doesnt match expected show.'
116+
assert episode.show() == show, 'episode.show() doesnt match expected show.'
117+
assert episode.season() == season, 'episode.season() doesnt match expected season.'
118+
119+
120+
def test_008_mark_movie_watched(plex):
121+
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
122+
movie.markUnwatched()
123+
log(2, 'Marking movie watched: %s' % movie)
124+
log(2, 'View count: %s' % movie.viewCount)
125+
movie.markWatched()
126+
log(2, 'View count: %s' % movie.viewCount)
127+
assert movie.viewCount == 1, 'View count 0 after watched.'
128+
movie.markUnwatched()
129+
log(2, 'View count: %s' % movie.viewCount)
130+
assert movie.viewCount == 0, 'View count 1 after unwatched.'
131+
132+
133+
def test_009_refresh(plex):
134+
shows = plex.library.section(MOVIE_SECTION)
135+
shows.refresh()
136+
137+
138+
def test_010_playQueues(plex):
139+
episode = plex.library.get(SHOW_TITLE).get(SHOW_EPISODE)
140+
playqueue = plex.createPlayQueue(episode)
141+
assert len(playqueue.items) == 1, 'No items in play queue.'
142+
assert playqueue.items[0].title == SHOW_EPISODE, 'Wrong show queued.'
143+
assert playqueue.playQueueID, 'Play queue ID not set.'
144+
145+
146+
def test_011_play_media(plex):
147+
# Make sure the client is turned on!
148+
episode = plex.library.get(SHOW_TITLE).get(SHOW_EPISODE)
149+
client = plex.client(PLEX_CLIENT)
150+
client.playMedia(episode); time.sleep(10)
151+
client.pause(); time.sleep(3)
152+
client.stepForward(); time.sleep(3)
153+
client.play(); time.sleep(3)
154+
client.stop(); time.sleep(3)
155+
movie = plex.library.get(MOVIE_TITLE)
156+
movie.play(client); time.sleep(10)
157+
client.stop()
158+
159+
160+
def test_012_myplex_account(plex):
161+
account = plex.account()
162+
print account.__dict__
163+
164+
165+
def test_013_list_media_files(plex):
166+
# Fetch file names from the tv show
167+
episode_files = []
168+
episode = plex.library.get(SHOW_TITLE).episodes()[-1]
169+
log(2, 'Episode Files: %s' % episode)
170+
for media in episode.media:
171+
for part in media.parts:
172+
log(4, part.file)
173+
episode_files.append(part.file)
174+
assert filter(None, episode_files), 'No show files have been listed.'
175+
# Fetch file names from the movie
176+
movie_files = []
177+
movie = plex.library.get(MOVIE_TITLE)
178+
log(2, 'Movie Files: %s' % movie)
179+
for media in movie.media:
180+
for part in media.parts:
181+
log(4, part.file)
182+
movie_files.append(part.file)
183+
assert filter(None, movie_files), 'No movie files have been listed.'
184+
185+
186+
def test_014_list_video_tags(plex):
187+
movie = plex.library.get(MOVIE_TITLE)
188+
log(2, 'Countries: %s' % movie.countries[0:3])
189+
log(2, 'Directors: %s' % movie.directors[0:3])
190+
log(2, 'Genres: %s' % movie.genres[0:3])
191+
log(2, 'Producers: %s' % movie.producers[0:3])
192+
log(2, 'Actors: %s' % movie.actors[0:3])
193+
log(2, 'Writers: %s' % movie.writers[0:3])
194+
assert filter(None, movie.countries), 'No countries listed for movie.'
195+
assert filter(None, movie.directors), 'No directors listed for movie.'
196+
assert filter(None, movie.genres), 'No genres listed for movie.'
197+
assert filter(None, movie.producers), 'No producers listed for movie.'
198+
assert filter(None, movie.actors), 'No actors listed for movie.'
199+
assert filter(None, movie.writers), 'No writers listed for movie.'
200+
log(2, 'List movies with same director: %s' % movie.directors[0])
201+
related = movie.directors[0].related()
202+
log(4, related[0:3])
203+
assert movie in related, 'Movie was not found in related directors search.'
204+
205+
206+
if __name__ == '__main__':
207+
parser = argparse.ArgumentParser(description='Run PlexAPI tests.')
208+
parser.add_argument('-s', '--server', help='Name of the Plex server (requires user/pass).')
209+
parser.add_argument('-u', '--username', help='Username for the Plex server.')
210+
parser.add_argument('-p', '--password', help='Password for the Plex server.')
211+
parser.add_argument('-n', '--name', help='Only run tests containing this string. Leave blank to run all tests.')
212+
args = parser.parse_args()
213+
run_tests(__name__, args)

‎examples/utils.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Test Library Functions
3+
"""
4+
import inspect, sys
5+
import datetime, time
6+
from plexapi import server
7+
from plexapi.myplex import MyPlexUser
8+
9+
10+
def log(indent, message):
11+
dt = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
12+
print('%s: %s%s' % (dt, ' '*indent, message))
13+
14+
15+
def fetch_server(args):
16+
if args.server:
17+
user = MyPlexUser(args.username, args.password)
18+
return user.getServer(args.server).connect()
19+
return server.PlexServer()
20+
21+
22+
def iter_tests(module, args):
23+
module = sys.modules[module]
24+
for func in sorted(module.__dict__.values()):
25+
if inspect.isfunction(func) and inspect.getmodule(func) == module:
26+
name = func.__name__
27+
if name.startswith('test_') or name.startswith('example_') and (not args.name or args.name in name):
28+
yield func
29+
30+
31+
def run_tests(module, args):
32+
plex = fetch_server(args)
33+
tests = {'passed':0, 'failed':0}
34+
for test in iter_tests(module, args):
35+
startqueries = server.TOTAL_QUERIES
36+
starttime = time.time()
37+
log(0, test.__name__)
38+
try:
39+
test(plex)
40+
tests['passed'] += 1
41+
except Exception, err:
42+
log(2, 'FAIL!: %s' % err)
43+
tests['failed'] += 1
44+
runtime = time.time() - starttime
45+
log(2, 'Runtime: %.3fs' % runtime)
46+
log(2, 'Queries: %s' % (server.TOTAL_QUERIES - startqueries))
47+
log(0, '')
48+
log(0, 'Tests Run: %s' % sum(tests.values()))
49+
log(0, 'Tests Passed: %s' % tests['passed'])
50+
log(0, 'Tests Failed: %s' % tests['failed'])
51+
if not tests['failed']:
52+
log(0, '')
53+
log(0, 'EVERYTHING OK!! :)')
54+
raise SystemExit(tests['failed'])

‎plexapi/__init__.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
PlexAPI
3+
"""
4+
import logging, os, platform
5+
from logging.handlers import RotatingFileHandler
6+
from uuid import getnode
7+
8+
PROJECT = 'PlexAPI'
9+
VERSION = '0.9.4'
10+
TIMEOUT = 5
11+
12+
# Plex Header Configuation
13+
X_PLEX_PLATFORM = platform.uname()[0] # Platform name, eg iOS, MacOSX, Android, LG, etc
14+
X_PLEX_PLATFORM_VERSION = platform.uname()[2] # Operating system version, eg 4.3.1, 10.6.7, 3.2
15+
X_PLEX_PROVIDES = 'controller' # one or more of [player, controller, server]
16+
X_PLEX_PRODUCT = PROJECT # Plex application name, eg Laika, Plex Media Server, Media Link
17+
X_PLEX_VERSION = VERSION # Plex application version number
18+
X_PLEX_DEVICE = platform.platform() # Device name and model number, eg iPhone3,2, Motorola XOOM, LG5200TV
19+
X_PLEX_IDENTIFIER = str(hex(getnode())) # UUID, serial number, or other number unique per device
20+
BASE_HEADERS = {
21+
'X-Plex-Platform': X_PLEX_PLATFORM,
22+
'X-Plex-Platform-Version': X_PLEX_PLATFORM_VERSION,
23+
'X-Plex-Provides': X_PLEX_PROVIDES,
24+
'X-Plex-Product': X_PLEX_PRODUCT,
25+
'X-Plex-Version': X_PLEX_VERSION,
26+
'X-Plex-Device': X_PLEX_DEVICE,
27+
'X-Plex-Client-Identifier': X_PLEX_IDENTIFIER,
28+
}
29+
30+
# Logging Configuration
31+
log = logging.getLogger('plexapi')
32+
logfile = os.path.join('/tmp', 'plexapi.log')
33+
logformat = logging.Formatter('%(asctime)s %(module)-12s %(levelname)-6s %(message)s')
34+
filehandler = RotatingFileHandler(logfile, 'a', 512000, 3)
35+
filehandler.setFormatter(logformat)
36+
log.addHandler(filehandler)
37+
log.setLevel(logging.INFO)

‎plexapi/client.py

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
PlexAPI Client
3+
See: https://code.google.com/p/plex-api/w/list
4+
"""
5+
import requests
6+
from requests.status_codes import _codes as codes
7+
from plexapi import TIMEOUT, log, utils, BASE_HEADERS
8+
from plexapi.exceptions import BadRequest
9+
from xml.etree import ElementTree
10+
11+
SERVER = 'server'
12+
CLIENT = 'client'
13+
14+
15+
class Client(object):
16+
17+
def __init__(self, server, data):
18+
self.server = server
19+
self.name = data.attrib.get('name')
20+
self.host = data.attrib.get('host')
21+
self.address = data.attrib.get('address')
22+
self.port = data.attrib.get('port')
23+
self.machineIdentifier = data.attrib.get('machineIdentifier')
24+
self.version = data.attrib.get('version')
25+
self.protocol = data.attrib.get('protocol')
26+
self.product = data.attrib.get('product')
27+
self.deviceClass = data.attrib.get('deviceClass')
28+
self.protocolVersion = data.attrib.get('protocolVersion')
29+
self.protocolCapabilities = data.attrib.get('protocolCapabilities', '').split(',')
30+
self._sendCommandsTo = SERVER
31+
32+
def sendCommandsTo(self, value):
33+
self._sendCommandsTo = value
34+
35+
def sendCommand(self, command, args=None, sendTo=None):
36+
sendTo = sendTo or self._sendCommandsTo
37+
if sendTo == CLIENT:
38+
return self.sendClientCommand(command, args)
39+
return self.sendServerCommand(command, args)
40+
41+
def sendClientCommand(self, command, args=None):
42+
url = '%s%s' % (self.url(command), utils.joinArgs(args))
43+
log.info('GET %s', url)
44+
response = requests.get(url, timeout=TIMEOUT)
45+
if response.status_code != requests.codes.ok:
46+
codename = codes.get(response.status_code)[0]
47+
raise BadRequest('(%s) %s' % (response.status_code, codename))
48+
data = response.text.encode('utf8')
49+
return ElementTree.fromstring(data) if data else None
50+
51+
def sendServerCommand(self, command, args=None):
52+
path = '/system/players/%s/%s%s' % (self.address, command, utils.joinArgs(args))
53+
self.server.query(path)
54+
55+
def url(self, path):
56+
return 'http://%s:%s/player/%s' % (self.address, self.port, path.lstrip('/'))
57+
58+
# Navigation Commands
59+
def moveUp(self): self.sendCommand('navigation/moveUp')
60+
def moveDown(self): self.sendCommand('navigation/moveDown')
61+
def moveLeft(self): self.sendCommand('navigation/moveLeft')
62+
def moveRight(self): self.sendCommand('navigation/moveRight')
63+
def pageUp(self): self.sendCommand('navigation/pageUp')
64+
def pageDown(self): self.sendCommand('navigation/pageDown')
65+
def nextLetter(self): self.sendCommand('navigation/nextLetter')
66+
def previousLetter(self): self.sendCommand('navigation/previousLetter')
67+
def select(self): self.sendCommand('navigation/select')
68+
def back(self): self.sendCommand('navigation/back')
69+
def contextMenu(self): self.sendCommand('navigation/contextMenu')
70+
def toggleOSD(self): self.sendCommand('navigation/toggleOSD')
71+
72+
# Playback Commands
73+
def play(self): self.sendCommand('playback/play')
74+
def pause(self): self.sendCommand('playback/pause')
75+
def stop(self): self.sendCommand('playback/stop')
76+
def stepForward(self): self.sendCommand('playback/stepForward')
77+
def bigStepForward(self): self.sendCommand('playback/bigStepForward')
78+
def stepBack(self): self.sendCommand('playback/stepBack')
79+
def bigStepBack(self): self.sendCommand('playback/bigStepBack')
80+
def skipNext(self): self.sendCommand('playback/skipNext')
81+
def skipPrevious(self): self.sendCommand('playback/skipPrevious')
82+
83+
def playMedia(self, video, viewOffset=0):
84+
playqueue = self.server.createPlayQueue(video)
85+
self.sendCommand('playback/playMedia', {
86+
'machineIdentifier': self.server.machineIdentifier,
87+
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
88+
'key': video.key,
89+
'offset': int(viewOffset),
90+
})
91+
92+
def timeline(self):
93+
"""
94+
Returns an XML ElementTree object corresponding to the timeline for
95+
this client. Holds the information about what media is playing on this
96+
client.
97+
"""
98+
99+
url = self.url('timeline/poll')
100+
params = {
101+
'wait': 1,
102+
'commandID': 4,
103+
}
104+
xml_text = requests.get(url, params=params, headers=BASE_HEADERS).text
105+
return ElementTree.fromstring(xml_text)
106+
107+
def isPlayingMedia(self):
108+
"""
109+
Returns True if any of the media types for this client have the status
110+
of "playing", False otherwise. Also returns True if media is paused.
111+
"""
112+
113+
timeline = self.timeline()
114+
for media_type in timeline:
115+
if media_type.get('state') == 'playing':
116+
return True
117+
return False
118+
119+
# def rewind(self): self.sendCommand('playback/rewind')
120+
# def fastForward(self): self.sendCommand('playback/fastForward')
121+
# def playFile(self): pass
122+
# def screenshot(self): pass
123+
# def sendString(self): pass
124+
# def sendKey(self): pass
125+
# def sendVirtualKey(self): pass

‎plexapi/exceptions.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
PlexAPI Exceptions
3+
"""
4+
5+
class BadRequest(Exception):
6+
pass
7+
8+
class NotFound(Exception):
9+
pass
10+
11+
class UnknownType(Exception):
12+
pass
13+
14+
class Unsupported(Exception):
15+
pass

‎plexapi/library.py

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""
2+
PlexLibrary
3+
"""
4+
from plexapi import video, utils
5+
from plexapi.exceptions import NotFound
6+
7+
8+
class Library(object):
9+
10+
def __init__(self, server, data):
11+
self.server = server
12+
self.identifier = data.attrib.get('identifier')
13+
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
14+
self.title1 = data.attrib.get('title1')
15+
self.title2 = data.attrib.get('title2')
16+
17+
def __repr__(self):
18+
return '<Library:%s>' % self.title1.encode('utf8')
19+
20+
def sections(self):
21+
items = []
22+
SECTION_TYPES = {MovieSection.TYPE:MovieSection, ShowSection.TYPE:ShowSection}
23+
path = '/library/sections'
24+
for elem in self.server.query(path):
25+
stype = elem.attrib['type']
26+
if stype in SECTION_TYPES:
27+
cls = SECTION_TYPES[stype]
28+
items.append(cls(self.server, elem, path))
29+
return items
30+
31+
def section(self, title=None):
32+
for item in self.sections():
33+
if item.title == title:
34+
return item
35+
raise NotFound('Invalid library section: %s' % title)
36+
37+
def all(self):
38+
return video.list_items(self.server, '/library/all')
39+
40+
def onDeck(self):
41+
return video.list_items(self.server, '/library/onDeck')
42+
43+
def recentlyAdded(self):
44+
return video.list_items(self.server, '/library/recentlyAdded')
45+
46+
def get(self, title):
47+
return video.find_item(self.server, '/library/all', title)
48+
49+
def search(self, title, filter='all', vtype=None, **tags):
50+
""" Search all available content.
51+
title: Title to search (pass None to search all titles).
52+
filter: One of {'all', 'onDeck', 'recentlyAdded'}.
53+
videotype: One of {'movie', 'show', 'season', 'episode'}.
54+
tags: One of {country, director, genre, producer, actor, writer}.
55+
"""
56+
args = {}
57+
if title: args['title'] = title
58+
if vtype: args['type'] = video.search_type(vtype)
59+
for tag, obj in tags.iteritems():
60+
args[tag] = obj.id
61+
query = '/library/%s%s' % (filter, utils.joinArgs(args))
62+
return video.list_items(self.server, query)
63+
64+
def cleanBundles(self):
65+
self.server.query('/library/clean/bundles')
66+
67+
def emptyTrash(self):
68+
for section in self.sections():
69+
section.emptyTrash()
70+
71+
def optimize(self):
72+
self.server.query('/library/optimize')
73+
74+
def refresh(self):
75+
self.server.query('/library/sections/all/refresh')
76+
77+
78+
class LibrarySection(object):
79+
80+
def __init__(self, server, data, initpath):
81+
self.server = server
82+
self.initpath = initpath
83+
self.type = data.attrib.get('type')
84+
self.key = data.attrib.get('key')
85+
self.title = data.attrib.get('title')
86+
self.scanner = data.attrib.get('scanner')
87+
self.language = data.attrib.get('language')
88+
89+
def __repr__(self):
90+
title = self.title.replace(' ','.')[0:20]
91+
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
92+
93+
def _primaryList(self, key):
94+
return video.list_items(self.server, '/library/sections/%s/%s' % (self.key, key))
95+
96+
def _secondaryList(self, key, input=None):
97+
choices = list_choices(self.server, '/library/sections/%s/%s' % (self.key, key))
98+
if not input:
99+
return choices.keys()
100+
return video.list_items(self.server, '/library/sections/%s/%s/%s' % (self.key, key, choices[input]))
101+
102+
def all(self):
103+
return self._primaryList('all')
104+
105+
def newest(self):
106+
return self._primaryList('newest')
107+
108+
def onDeck(self):
109+
return self._primaryList('onDeck')
110+
111+
def recentlyAdded(self):
112+
return self._primaryList('recentlyAdded')
113+
114+
def recentlyViewed(self):
115+
return self._primaryList('recentlyViewed')
116+
117+
def unwatched(self):
118+
return self._primaryList('unwatched')
119+
120+
def contentRating(self, input=None):
121+
return self._secondaryList('contentRating', input)
122+
123+
def firstCharacter(self, input=None):
124+
return self._secondaryList('firstCharacter', input)
125+
126+
def genre(self, input=None):
127+
return self._secondaryList('genre', input)
128+
129+
def year(self, input=None):
130+
return self._secondaryList('year', input)
131+
132+
def get(self, title):
133+
path = '/library/sections/%s/all' % self.key
134+
return video.find_item(self.server, path, title)
135+
136+
def search(self, title, filter='all', vtype=None, **tags):
137+
""" Search section content.
138+
title: Title to search (pass None to search all titles).
139+
filter: One of {'all', 'newest', 'onDeck', 'recentlyAdded', 'recentlyViewed', 'unwatched'}.
140+
videotype: One of {'movie', 'show', 'season', 'episode'}.
141+
tags: One of {country, director, genre, producer, actor, writer}.
142+
"""
143+
args = {}
144+
if title: args['title'] = title
145+
if vtype: args['type'] = video.search_type(vtype)
146+
for tag, obj in tags.iteritems():
147+
args[tag] = obj.id
148+
query = '/library/sections/%s/%s%s' % (self.key, filter, utils.joinArgs(args))
149+
return video.list_items(self.server, query)
150+
151+
def analyze(self):
152+
self.server.query('/library/sections/%s/analyze' % self.key)
153+
154+
def emptyTrash(self):
155+
self.server.query('/library/sections/%s/emptyTrash' % self.key)
156+
157+
def refresh(self):
158+
self.server.query('/library/sections/%s/refresh' % self.key)
159+
160+
161+
class MovieSection(LibrarySection):
162+
TYPE = 'movie'
163+
164+
def actor(self, input=None):
165+
return self._secondaryList('actor', input)
166+
167+
def country(self, input=None):
168+
return self._secondaryList('country', input)
169+
170+
def decade(self, input=None):
171+
return self._secondaryList('decade', input)
172+
173+
def director(self, input=None):
174+
return self._secondaryList('director', input)
175+
176+
def rating(self, input=None):
177+
return self._secondaryList('rating', input)
178+
179+
def resolution(self, input=None):
180+
return self._secondaryList('resolution', input)
181+
182+
def search(self, title, filter='all', **tags):
183+
return super(MovieSection, self).search(title, filter=filter, vtype=video.Movie.TYPE, **tags)
184+
185+
186+
class ShowSection(LibrarySection):
187+
TYPE = 'show'
188+
189+
def recentlyViewedShows(self):
190+
return self._primaryList('recentlyViewedShows')
191+
192+
def search(self, title, filter='all', **tags):
193+
return super(ShowSection, self).search(title, filter=filter, vtype=video.Show.TYPE, **tags)
194+
195+
def searchEpisodes(self, title, filter='all', **tags):
196+
return super(ShowSection, self).search(title, filter=filter, vtype=video.Episode.TYPE, **tags)
197+
198+
199+
def list_choices(server, path):
200+
return {c.attrib['title']:c.attrib['key'] for c in server.query(path)}

‎plexapi/media.py

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
PlexAPI Media
3+
"""
4+
from plexapi.utils import cast
5+
6+
7+
class Media(object):
8+
TYPE = 'Media'
9+
10+
def __init__(self, server, data, initpath, video):
11+
self.server = server
12+
self.initpath = initpath
13+
self.video = video
14+
self.videoResolution = data.attrib.get('videoResolution')
15+
self.id = cast(int, data.attrib.get('id'))
16+
self.duration = cast(int, data.attrib.get('duration'))
17+
self.bitrate = cast(int, data.attrib.get('bitrate'))
18+
self.width = cast(int, data.attrib.get('width'))
19+
self.height = cast(int, data.attrib.get('height'))
20+
self.aspectRatio = cast(float, data.attrib.get('aspectRatio'))
21+
self.audioChannels = cast(int, data.attrib.get('audioChannels'))
22+
self.audioCodec = data.attrib.get('audioCodec')
23+
self.videoCodec = data.attrib.get('videoCodec')
24+
self.container = data.attrib.get('container')
25+
self.videoFrameRate = data.attrib.get('videoFrameRate')
26+
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming'))
27+
self.optimizedForStreaming = cast(bool, data.attrib.get('has64bitOffsets'))
28+
self.parts = [MediaPart(server, elem, initpath, self) for elem in data]
29+
30+
def __repr__(self):
31+
title = self.video.title.replace(' ','.')[0:20]
32+
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
33+
34+
35+
class MediaPart(object):
36+
TYPE = 'Part'
37+
38+
def __init__(self, server, data, initpath, media):
39+
self.server = server
40+
self.initpath = initpath
41+
self.media = media
42+
self.id = cast(int, data.attrib.get('id'))
43+
self.key = data.attrib.get('key')
44+
self.duration = cast(int, data.attrib.get('duration'))
45+
self.file = data.attrib.get('file')
46+
self.size = cast(int, data.attrib.get('size'))
47+
self.container = data.attrib.get('container')
48+
self.syncId = cast(int, data.attrib.get('syncId', '-1'))
49+
self.syncItemId = cast(int, data.attrib.get('syncItemId', '-1'))
50+
self.transcodeState = data.attrib.get('transcodeState', '')
51+
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming', '0'))
52+
self.streams = [
53+
MediaPartStream.parse(self.server, elem, self.initpath, self)
54+
for elem in data if elem.tag == MediaPartStream.TYPE
55+
]
56+
57+
def __repr__(self):
58+
return '<%s:%s>' % (self.__class__.__name__, self.id)
59+
60+
def selectedStream(self, stream_type):
61+
streams = filter(lambda x: stream_type == x.type, self.streams)
62+
selected = filter(lambda x: x.selected is True, streams)
63+
if len(selected) == 0:
64+
return None
65+
66+
return selected[0]
67+
68+
69+
class MediaPartStream(object):
70+
TYPE = 'Stream'
71+
72+
def __init__(self, server, data, initpath, part):
73+
self.server = server
74+
self.initpath = initpath
75+
self.part = part
76+
self.id = cast(int, data.attrib.get('id'))
77+
self.type = cast(int, data.attrib.get('streamType'))
78+
self.codec = data.attrib.get('codec')
79+
self.selected = cast(bool, data.attrib.get('selected', '0'))
80+
self.index = cast(int, data.attrib.get('index', '-1'))
81+
82+
@staticmethod
83+
def parse(server, data, initpath, part):
84+
STREAMCLS = {
85+
StreamVideo.TYPE: StreamVideo,
86+
StreamAudio.TYPE: StreamAudio,
87+
StreamSubtitle.TYPE: StreamSubtitle
88+
}
89+
90+
stype = cast(int, data.attrib.get('streamType'))
91+
cls = STREAMCLS.get(stype, MediaPartStream)
92+
# return generic MediaPartStream if type is unknown
93+
return cls(server, data, initpath, part)
94+
95+
def __repr__(self):
96+
return '<%s:%s>' % (self.__class__.__name__, self.id)
97+
98+
99+
class StreamVideo(MediaPartStream):
100+
TYPE = 1
101+
102+
def __init__(self, server, data, initpath, part):
103+
super(StreamVideo, self).__init__(server, data, initpath, part)
104+
self.bitrate = cast(int, data.attrib.get('bitrate'))
105+
self.language = data.attrib.get('langauge')
106+
self.languageCode = data.attrib.get('languageCode')
107+
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
108+
self.cabac = cast(int, data.attrib.get('cabac'))
109+
self.chromaSubsampling = data.attrib.get('chromaSubsampling')
110+
self.codecID = data.attrib.get('codecID')
111+
self.colorSpace = data.attrib.get('colorSpace')
112+
self.duration = cast(int, data.attrib.get('duration'))
113+
self.frameRate = cast(float, data.attrib.get('frameRate'))
114+
self.frameRateMode = data.attrib.get('frameRateMode')
115+
self.hasScallingMatrix = cast(bool, data.attrib.get('hasScallingMatrix'))
116+
self.height = cast(int, data.attrib.get('height'))
117+
self.level = cast(int, data.attrib.get('level'))
118+
self.profile = data.attrib.get('profile')
119+
self.refFrames = cast(int, data.attrib.get('refFrames'))
120+
self.scanType = data.attrib.get('scanType')
121+
self.title = data.attrib.get('title')
122+
self.width = cast(int, data.attrib.get('width'))
123+
124+
125+
class StreamAudio(MediaPartStream):
126+
TYPE = 2
127+
128+
def __init__(self, server, data, initpath, part):
129+
super(StreamAudio, self).__init__(server, data, initpath, part)
130+
self.channels = cast(int, data.attrib.get('channels'))
131+
self.bitrate = cast(int, data.attrib.get('bitrate'))
132+
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
133+
self.bitrateMode = data.attrib.get('bitrateMode')
134+
self.codecID = data.attrib.get('codecID')
135+
self.dialogNorm = cast(int, data.attrib.get('dialogNorm'))
136+
self.duration = cast(int, data.attrib.get('duration'))
137+
self.samplingRate = cast(int, data.attrib.get('samplingRate'))
138+
self.title = data.attrib.get('title')
139+
140+
141+
class StreamSubtitle(MediaPartStream):
142+
TYPE = 3
143+
144+
def __init__(self, server, data, initpath, part):
145+
super(StreamSubtitle, self).__init__(server, data, initpath, part)
146+
self.key = data.attrib.get('key')
147+
self.language = data.attrib.get('langauge')
148+
self.languageCode = data.attrib.get('languageCode')
149+
self.format = data.attrib.get('format')
150+
151+
152+
class VideoTag(object):
153+
TYPE = None
154+
155+
def __init__(self, server, data):
156+
self.server = server
157+
self.id = cast(int, data.attrib.get('id'))
158+
self.tag = data.attrib.get('tag')
159+
self.role = data.attrib.get('role')
160+
161+
def __repr__(self):
162+
tag = self.tag.replace(' ','.')[0:20]
163+
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, tag)
164+
165+
def related(self, vtype=None):
166+
return self.server.library.search(None, **{self.FILTER:self})
167+
168+
169+
class Country(VideoTag): TYPE='Country'; FILTER='country'
170+
class Director(VideoTag): TYPE = 'Director'; FILTER='director'
171+
class Genre(VideoTag): TYPE='Genre'; FILTER='genre'
172+
class Producer(VideoTag): TYPE = 'Producer'; FILTER='producer'
173+
class Actor(VideoTag): TYPE = 'Role'; FILTER='actor'
174+
class Writer(VideoTag): TYPE = 'Writer'; FILTER='writer'

‎plexapi/myplex.py

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""
2+
PlexAPI MyPlex
3+
"""
4+
import plexapi, requests
5+
from requests.status_codes import _codes as codes
6+
from threading import Thread
7+
from xml.etree import ElementTree
8+
from plexapi import TIMEOUT, log
9+
from plexapi.exceptions import BadRequest, NotFound
10+
from plexapi.utils import cast, toDatetime, Connection
11+
from plexapi.sync import SyncItem
12+
13+
14+
class MyPlexUser:
15+
""" Logs into my.plexapp.com to fetch account and token information. This
16+
useful to get a token if not on the local network.
17+
"""
18+
SIGNIN = 'https://my.plexapp.com/users/sign_in.xml'
19+
20+
def __init__(self, username, password):
21+
data = self._signin(username, password)
22+
self.email = data.attrib.get('email')
23+
self.id = data.attrib.get('id')
24+
self.thumb = data.attrib.get('thumb')
25+
self.username = data.attrib.get('username')
26+
self.title = data.attrib.get('title')
27+
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice')
28+
self.authenticationToken = data.attrib.get('authenticationToken')
29+
self.queueEmail = data.attrib.get('queueEmail')
30+
self.queueUid = data.attrib.get('queueUid')
31+
32+
def _signin(self, username, password):
33+
auth = (username, password)
34+
log.info('POST %s', self.SIGNIN)
35+
response = requests.post(self.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT)
36+
if response.status_code != requests.codes.created:
37+
codename = codes.get(response.status_code)[0]
38+
raise BadRequest('(%s) %s' % (response.status_code, codename))
39+
data = response.text.encode('utf8')
40+
return ElementTree.fromstring(data)
41+
42+
def servers(self):
43+
return MyPlexServer.fetchServers(self.authenticationToken)
44+
45+
def getServer(self, nameOrSourceTitle):
46+
search = nameOrSourceTitle.lower()
47+
for server in self.servers():
48+
if server.name and search == server.name.lower(): return server
49+
if server.sourceTitle and search == server.sourceTitle.lower(): return server
50+
raise NotFound('Unable to find server: %s' % nameOrSourceTitle)
51+
52+
def devices(self):
53+
return MyPlexDevice.fetchDevices(self.authenticationToken)
54+
55+
def getDevice(self, nameOrClientIdentifier):
56+
search = nameOrClientIdentifier.lower()
57+
for device in self.devices():
58+
device_name = device.name.lower()
59+
device_cid = device.clientIdentifier.lower()
60+
if search in (device_name, device_cid):
61+
return device
62+
raise NotFound('Unable to find device: %s' % nameOrClientIdentifier)
63+
64+
def syncDevices(self):
65+
return filter(lambda x: 'sync-target' in x.provides, self.devices())
66+
67+
68+
class MyPlexAccount:
69+
""" Represents myPlex account if you already have a connection to a server. """
70+
71+
def __init__(self, server, data):
72+
self.authToken = data.attrib.get('authToken')
73+
self.username = data.attrib.get('username')
74+
self.mappingState = data.attrib.get('mappingState')
75+
self.mappingError = data.attrib.get('mappingError')
76+
self.mappingErrorMessage = data.attrib.get('mappingErrorMessage')
77+
self.signInState = data.attrib.get('signInState')
78+
self.publicAddress = data.attrib.get('publicAddress')
79+
self.publicPort = data.attrib.get('publicPort')
80+
self.privateAddress = data.attrib.get('privateAddress')
81+
self.privatePort = data.attrib.get('privatePort')
82+
self.subscriptionFeatures = data.attrib.get('subscriptionFeatures')
83+
self.subscriptionActive = data.attrib.get('subscriptionActive')
84+
self.subscriptionState = data.attrib.get('subscriptionState')
85+
86+
def servers(self):
87+
return MyPlexServer.fetchServers(self.authToken)
88+
89+
def getServer(self, nameOrSourceTitle):
90+
for server in self.servers():
91+
if nameOrSourceTitle.lower() in [server.name.lower(), server.sourceTitle.lower()]:
92+
return server
93+
raise NotFound('Unable to find server: %s' % nameOrSourceTitle)
94+
95+
96+
class MyPlexServer:
97+
SERVERS = 'https://plex.tv/pms/servers.xml?includeLite=1'
98+
99+
def __init__(self, data):
100+
self.accessToken = data.attrib.get('accessToken')
101+
self.name = data.attrib.get('name')
102+
self.address = data.attrib.get('address')
103+
self.port = cast(int, data.attrib.get('port'))
104+
self.version = data.attrib.get('version')
105+
self.scheme = data.attrib.get('scheme')
106+
self.host = data.attrib.get('host')
107+
self.localAddresses = data.attrib.get('localAddresses', '').split(',')
108+
self.machineIdentifier = data.attrib.get('machineIdentifier')
109+
self.createdAt = toDatetime(data.attrib.get('createdAt'))
110+
self.updatedAt = toDatetime(data.attrib.get('updatedAt'))
111+
self.owned = cast(bool, data.attrib.get('owned'))
112+
self.synced = cast(bool, data.attrib.get('synced'))
113+
self.sourceTitle = data.attrib.get('sourceTitle', '')
114+
self.ownerId = cast(int, data.attrib.get('ownerId'))
115+
self.home = data.attrib.get('home')
116+
117+
def __repr__(self):
118+
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
119+
120+
def connect(self):
121+
# Create a list of addresses to try connecting to.
122+
# TODO: setup local addresses before external
123+
devices = MyPlexDevice.fetchDevices(self.accessToken)
124+
devices = filter(lambda x: x.clientIdentifier == self.machineIdentifier, devices)
125+
addresses = []
126+
if len(devices) == 1:
127+
addresses += devices[0].connections
128+
else:
129+
addresses.append(Connection(self.address, self.port))
130+
if self.owned:
131+
for local in self.localAddresses:
132+
addresses.append(Connection(local, self.port))
133+
# Attempt to connect to all known addresses in parellel to save time, but
134+
# only return the first server (in order) that provides a response.
135+
threads = [None] * len(addresses)
136+
results = [None] * len(addresses)
137+
for i in range(len(addresses)):
138+
args = (addresses[i], results, i)
139+
threads[i] = Thread(target=self._connect, args=args)
140+
threads[i].start()
141+
for thread in threads:
142+
thread.join()
143+
results = filter(None, results)
144+
if results: return results[0]
145+
raise NotFound('Unable to connect to server: %s' % self.name)
146+
147+
def _connect(self, address, results, i):
148+
from plexapi.server import PlexServer
149+
try:
150+
results[i] = PlexServer(address.addr, address.port, self.accessToken)
151+
except NotFound:
152+
results[i] = None
153+
154+
@classmethod
155+
def fetchServers(cls, token):
156+
headers = plexapi.BASE_HEADERS
157+
headers['X-Plex-Token'] = token
158+
log.info('GET %s?X-Plex-Token=%s', cls.SERVERS, token)
159+
response = requests.get(cls.SERVERS, headers=headers, timeout=TIMEOUT)
160+
data = ElementTree.fromstring(response.text.encode('utf8'))
161+
return [MyPlexServer(elem) for elem in data]
162+
163+
164+
class MyPlexDevice(object):
165+
DEVICES = 'https://my.plexapp.com/devices.xml'
166+
167+
def __init__(self, data):
168+
self.name = data.attrib.get('name')
169+
self.publicAddress = data.attrib.get('publicAddress')
170+
self.product = data.attrib.get('product')
171+
self.productVersion = data.attrib.get('productVersion')
172+
self.platform = data.attrib.get('platform')
173+
self.platformVersion = data.attrib.get('platformVersion')
174+
self.devices = data.attrib.get('device') # Whats going on here..
175+
self.model = data.attrib.get('model')
176+
self.vendor = data.attrib.get('vendor')
177+
self.provides = data.attrib.get('provides').split(',')
178+
self.clientIdentifier = data.attrib.get('clientIdentifier')
179+
self.version = data.attrib.get('version')
180+
self.id = cast(int, data.attrib.get('id'))
181+
self.token = data.attrib.get('token')
182+
self.createdAt = toDatetime(data.attrib.get('createdAt'))
183+
self.lastSeenAt = toDatetime(data.attrib.get('lastSeenAt'))
184+
self.screenResolution = data.attrib.get('screenResolution')
185+
self.screenDensity = data.attrib.get('screenDensity')
186+
self.connections = [Connection.from_xml(elem) for elem in data.iterfind('Connection')]
187+
self.syncList = [elem.attrib.copy() for elem in data.iterfind('SyncList')]
188+
self._syncItemsUrl = 'https://plex.tv/devices/{0}/sync_items.xml'.format(self.clientIdentifier)
189+
190+
def syncItems(self):
191+
headers = plexapi.BASE_HEADERS
192+
headers['X-Plex-Token'] = self.token
193+
response = requests.get(self._syncItemsUrl, headers=headers, timeout=TIMEOUT)
194+
data = ElementTree.fromstring(response.text.encode('utf8'))
195+
servers = MyPlexServer.fetchServers(self.token)
196+
return [SyncItem(self, elem, servers) for elem in data.find('SyncItems').iterfind('SyncItem')]
197+
198+
def __repr__(self):
199+
return '<{0}:{1}>'.format(self.__class__.__name__, self.name)
200+
201+
@classmethod
202+
def fetchDevices(cls, token):
203+
headers = plexapi.BASE_HEADERS
204+
headers['X-Plex-Token'] = token
205+
response = requests.get(MyPlexDevice.DEVICES, headers=headers, timeout=TIMEOUT)
206+
data = ElementTree.fromstring(response.text.encode('utf8'))
207+
return [MyPlexDevice(elem) for elem in data]
208+
209+
210+
if __name__ == '__main__':
211+
import sys
212+
myplex = MyPlexUser(sys.argv[1], sys.argv[2])
213+
server = myplex.getServer(sys.argv[3]).connect()
214+
print server.library.section("Movies").all()

‎plexapi/playqueue.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
PlexAPI Play PlayQueues
3+
"""
4+
import plexapi, requests
5+
from plexapi import video
6+
from plexapi import utils
7+
8+
9+
class PlayQueue(object):
10+
11+
def __init__(self, server, data, initpath):
12+
self.server = server
13+
self.initpath = initpath
14+
self.identifier = data.attrib.get('identifier')
15+
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
16+
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
17+
self.playQueueID = data.attrib.get('playQueueID')
18+
self.playQueueSelectedItemID = data.attrib.get('playQueueSelectedItemID')
19+
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
20+
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
21+
self.playQueueVersion = data.attrib.get('playQueueVersion')
22+
self.items = [video.build_item(server, elem, initpath) for elem in data]
23+
24+
@classmethod
25+
def create(cls, server, video, shuffle=0, continuous=0):
26+
# NOTE: I have not yet figured out what __GID__ is below or where the proper value
27+
# can be obtained. However, the good news is passing anything in seems to work.
28+
path = 'playQueues%s' % utils.joinArgs({
29+
'uri': 'library://__GID__/item/%s' % video.key,
30+
'key': video.key,
31+
'type': 'video',
32+
'shuffle': shuffle,
33+
'continuous': continuous,
34+
'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER,
35+
})
36+
data = server.query(path, method=requests.post)
37+
return cls(server, data, initpath=path)

‎plexapi/server.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
PlexServer
3+
"""
4+
import requests, urllib
5+
from requests.status_codes import _codes as codes
6+
from plexapi import BASE_HEADERS, TIMEOUT
7+
from plexapi import log, video
8+
from plexapi.client import Client
9+
from plexapi.exceptions import BadRequest, NotFound
10+
from plexapi.library import Library
11+
from plexapi.myplex import MyPlexAccount
12+
from plexapi.playqueue import PlayQueue
13+
from xml.etree import ElementTree
14+
15+
TOTAL_QUERIES = 0
16+
17+
18+
class PlexServer(object):
19+
20+
def __init__(self, address='localhost', port=32400, token=None):
21+
self.address = self._cleanAddress(address)
22+
self.port = port
23+
self.token = token
24+
data = self._connect()
25+
self.friendlyName = data.attrib.get('friendlyName')
26+
self.machineIdentifier = data.attrib.get('machineIdentifier')
27+
self.myPlex = bool(data.attrib.get('myPlex'))
28+
self.myPlexMappingState = data.attrib.get('myPlexMappingState')
29+
self.myPlexSigninState = data.attrib.get('myPlexSigninState')
30+
self.myPlexSubscription = data.attrib.get('myPlexSubscription')
31+
self.myPlexUsername = data.attrib.get('myPlexUsername')
32+
self.platform = data.attrib.get('platform')
33+
self.platformVersion = data.attrib.get('platformVersion')
34+
self.transcoderActiveVideoSessions = int(data.attrib.get('transcoderActiveVideoSessions'))
35+
self.updatedAt = int(data.attrib.get('updatedAt'))
36+
self.version = data.attrib.get('version')
37+
38+
def __repr__(self):
39+
return '<%s:%s:%s>' % (self.__class__.__name__, self.address, self.port)
40+
41+
def _cleanAddress(self, address):
42+
address = address.lower().strip('/')
43+
if address.startswith('http://'):
44+
address = address[8:]
45+
return address
46+
47+
def _connect(self):
48+
try:
49+
return self.query('/')
50+
except Exception, err:
51+
log.error('%s:%s: %s', self.address, self.port, err)
52+
raise NotFound('No server found at: %s:%s' % (self.address, self.port))
53+
54+
@property
55+
def library(self):
56+
return Library(self, self.query('/library/'))
57+
58+
def account(self):
59+
data = self.query('/myplex/account')
60+
return MyPlexAccount(self, data)
61+
62+
def clients(self):
63+
items = []
64+
for elem in self.query('/clients'):
65+
items.append(Client(self, elem))
66+
return items
67+
68+
def client(self, name):
69+
for elem in self.query('/clients'):
70+
if elem.attrib.get('name').lower() == name.lower():
71+
return Client(self, elem)
72+
raise NotFound('Unknown client name: %s' % name)
73+
74+
def createPlayQueue(self, video):
75+
return PlayQueue.create(self, video)
76+
77+
def headers(self):
78+
headers = BASE_HEADERS
79+
if self.token:
80+
headers['X-Plex-Token'] = self.token
81+
return headers
82+
83+
def query(self, path, method=requests.get):
84+
global TOTAL_QUERIES; TOTAL_QUERIES += 1
85+
url = self.url(path)
86+
log.info('%s %s%s', method.__name__.upper(), url, '?X-Plex-Token=%s' % self.token if self.token else '')
87+
response = method(url, headers=self.headers(), timeout=TIMEOUT)
88+
if response.status_code not in [200, 201]:
89+
codename = codes.get(response.status_code)[0]
90+
raise BadRequest('(%s) %s' % (response.status_code, codename))
91+
data = response.text.encode('utf8')
92+
return ElementTree.fromstring(data) if data else None
93+
94+
def search(self, query, videotype=None):
95+
query = urllib.quote(query)
96+
items = video.list_items(self, '/search?query=%s' % query)
97+
if videotype:
98+
return [item for item in items if item.type == videotype]
99+
return items
100+
101+
def url(self, path):
102+
return 'http://%s:%s/%s' % (self.address, self.port, path.lstrip('/'))

‎plexapi/sync.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
PlexAPI Sync
3+
"""
4+
import requests
5+
from plexapi.exceptions import NotFound
6+
from plexapi.video import list_items
7+
from plexapi.utils import cast
8+
9+
10+
class SyncItem(object):
11+
def __init__(self, device, data, servers=None):
12+
self.device = device
13+
self.servers = servers
14+
self.id = cast(int, data.attrib.get('id'))
15+
self.version = cast(int, data.attrib.get('version'))
16+
self.rootTitle = data.attrib.get('rootTitle')
17+
self.title = data.attrib.get('title')
18+
self.metadataType = data.attrib.get('metadataType')
19+
self.machineIdentifier = data.find('Server').get('machineIdentifier')
20+
self.status = data.find('Status').attrib.copy()
21+
self.MediaSettings = data.find('MediaSettings').attrib.copy()
22+
self.policy = data.find('Policy').attrib.copy()
23+
self.location = data.find('Location').attrib.copy()
24+
25+
def __repr__(self):
26+
return '<{0}:{1}>'.format(self.__class__.__name__, self.id)
27+
28+
def server(self):
29+
server = filter(lambda x: x.machineIdentifier == self.machineIdentifier, self.servers)
30+
if 0 == len(server):
31+
raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier)
32+
33+
return server[0]
34+
35+
def getMedia(self):
36+
server = self.server().connect()
37+
items = list_items(server, '/sync/items/{0}'.format(self.id))
38+
return items
39+
40+
def markAsDone(self, sync_id):
41+
server = self.server().connect()
42+
uri = '/sync/{0}/{1}/files/{2}/downloaded'.format(self.device.uuid, server.machineIdentifier, sync_id)
43+
server.query(uri, method=requests.put)

‎plexapi/utils.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
PlexAPI Utils
3+
"""
4+
import urllib
5+
from datetime import datetime
6+
7+
NA = '__NA__' # Value not available
8+
9+
10+
class PlexPartialObject(object):
11+
""" Not all objects in the Plex listings return the complete list of
12+
elements for the object. This object will allow you to assume each
13+
object is complete, and if the specified value you request is None
14+
it will fetch the full object automatically and update itself.
15+
"""
16+
17+
def __init__(self, server, data, initpath):
18+
self.server = server
19+
self.initpath = initpath
20+
self._loadData(data)
21+
22+
def __getattr__(self, attr):
23+
if self.isPartialObject():
24+
self.reload()
25+
return self.__dict__.get(attr)
26+
27+
def __setattr__(self, attr, value):
28+
if value != NA:
29+
super(PlexPartialObject, self).__setattr__(attr, value)
30+
31+
def _loadData(self, data):
32+
raise Exception('Abstract method not implemented.')
33+
34+
def isFullObject(self):
35+
return self.initpath == self.key
36+
37+
def isPartialObject(self):
38+
return self.initpath != self.key
39+
40+
def reload(self):
41+
data = self.server.query(self.key)
42+
self.initpath = self.key
43+
self._loadData(data[0])
44+
45+
46+
class Connection(object):
47+
def __init__(self, addr, port):
48+
self.addr = addr
49+
self.port = int(port)
50+
51+
@classmethod
52+
def from_xml(cls, data):
53+
uri = data.attrib.get('uri')
54+
addr, port = [elem.strip('/') for elem in uri.split(':')[1:]]
55+
return Connection(addr, port)
56+
57+
def __repr__(self):
58+
return '<Connection:{0}:{1}>'.format(self.addr, self.port)
59+
60+
61+
def cast(func, value):
62+
if value not in [None, NA]:
63+
if func == bool:
64+
value = int(value)
65+
value = func(value)
66+
return value
67+
68+
69+
def joinArgs(args):
70+
if not args: return ''
71+
arglist = []
72+
for key in sorted(args, key=lambda x:x.lower()):
73+
value = str(args[key])
74+
arglist.append('%s=%s' % (key, urllib.quote(value)))
75+
return '?%s' % '&'.join(arglist)
76+
77+
78+
def toDatetime(value, format=None):
79+
if value and value != NA:
80+
if format: value = datetime.strptime(value, format)
81+
else: value = datetime.fromtimestamp(int(value))
82+
return value
83+
84+
85+
def lazyproperty(func):
86+
""" Decorator: Memoize method result. """
87+
attr = '_lazy_%s' % func.__name__
88+
@property
89+
def wrapper(self):
90+
if not hasattr(self, attr):
91+
setattr(self, attr, func(self))
92+
return getattr(self, attr)
93+
return wrapper

‎plexapi/video.py

+216
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""
2+
PlexVideo
3+
"""
4+
from plexapi.media import Media, Country, Director, Genre, Producer, Actor, Writer
5+
from plexapi.exceptions import NotFound, UnknownType
6+
from plexapi.utils import PlexPartialObject, NA
7+
from plexapi.utils import cast, toDatetime
8+
9+
10+
class Video(PlexPartialObject):
11+
TYPE = None
12+
13+
def __eq__(self, other):
14+
return self.type == other.type and self.key == other.key
15+
16+
def __repr__(self):
17+
title = self.title.replace(' ','.')[0:20]
18+
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
19+
20+
def _loadData(self, data):
21+
self.type = data.attrib.get('type', NA)
22+
self.key = data.attrib.get('key', NA)
23+
self.ratingKey = data.attrib.get('ratingKey', NA)
24+
self.title = data.attrib.get('title', NA)
25+
self.summary = data.attrib.get('summary', NA)
26+
self.art = data.attrib.get('art', NA)
27+
self.thumb = data.attrib.get('thumb', NA)
28+
self.addedAt = toDatetime(data.attrib.get('addedAt', NA))
29+
self.updatedAt = toDatetime(data.attrib.get('updatedAt', NA))
30+
self.lastViewedAt = toDatetime(data.attrib.get('lastViewedAt', NA))
31+
self.index = data.attrib.get('index', NA)
32+
self.parentIndex = data.attrib.get('parentIndex', NA)
33+
if self.isFullObject():
34+
# These are auto-populated when requested
35+
self.media = [Media(self.server, elem, self.initpath, self) for elem in data if elem.tag == Media.TYPE]
36+
self.countries = [Country(self.server, elem) for elem in data if elem.tag == Country.TYPE]
37+
self.directors = [Director(self.server, elem) for elem in data if elem.tag == Director.TYPE]
38+
self.genres = [Genre(self.server, elem) for elem in data if elem.tag == Genre.TYPE]
39+
self.producers = [Producer(self.server, elem) for elem in data if elem.tag == Producer.TYPE]
40+
self.actors = [Actor(self.server, elem) for elem in data if elem.tag == Actor.TYPE]
41+
self.writers = [Writer(self.server, elem) for elem in data if elem.tag == Writer.TYPE]
42+
43+
def iter_parts(self):
44+
for media in self.media:
45+
for part in media.parts:
46+
yield part
47+
48+
def analyze(self):
49+
self.server.query('/%s/analyze' % self.key)
50+
51+
def markWatched(self):
52+
path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
53+
self.server.query(path)
54+
self.reload()
55+
56+
def markUnwatched(self):
57+
path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
58+
self.server.query(path)
59+
self.reload()
60+
61+
def play(self, client):
62+
client.playMedia(self)
63+
64+
def refresh(self):
65+
self.server.query('/%s/refresh' % self.key)
66+
67+
68+
class Movie(Video):
69+
TYPE = 'movie'
70+
71+
def _loadData(self, data):
72+
super(Movie, self)._loadData(data)
73+
self.studio = data.attrib.get('studio', NA)
74+
self.contentRating = data.attrib.get('contentRating', NA)
75+
self.rating = data.attrib.get('rating', NA)
76+
self.viewCount = cast(int, data.attrib.get('viewCount', 0))
77+
self.viewOffset = cast(int, data.attrib.get('viewOffset', 0))
78+
self.year = cast(int, data.attrib.get('year', NA))
79+
self.tagline = data.attrib.get('tagline', NA)
80+
self.duration = cast(int, data.attrib.get('duration', NA))
81+
self.originallyAvailableAt = toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
82+
self.primaryExtraKey = data.attrib.get('primaryExtraKey', NA)
83+
84+
85+
class Show(Video):
86+
TYPE = 'show'
87+
88+
def _loadData(self, data):
89+
super(Show, self)._loadData(data)
90+
self.studio = data.attrib.get('studio', NA)
91+
self.contentRating = data.attrib.get('contentRating', NA)
92+
self.rating = data.attrib.get('rating', NA)
93+
self.year = cast(int, data.attrib.get('year', NA))
94+
self.banner = data.attrib.get('banner', NA)
95+
self.theme = data.attrib.get('theme', NA)
96+
self.duration = cast(int, data.attrib.get('duration', NA))
97+
self.originallyAvailableAt = toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
98+
self.leafCount = cast(int, data.attrib.get('leafCount', NA))
99+
self.viewedLeafCount = cast(int, data.attrib.get('viewedLeafCount', NA))
100+
self.childCount = cast(int, data.attrib.get('childCount', NA))
101+
102+
def seasons(self):
103+
path = '/library/metadata/%s/children' % self.ratingKey
104+
return list_items(self.server, path, Season.TYPE)
105+
106+
def season(self, title):
107+
path = '/library/metadata/%s/children' % self.ratingKey
108+
return find_item(self.server, path, title)
109+
110+
def episodes(self):
111+
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
112+
return list_items(self.server, leavesKey)
113+
114+
def episode(self, title):
115+
path = '/library/metadata/%s/allLeaves' % self.ratingKey
116+
return find_item(self.server, path, title)
117+
118+
def get(self, title):
119+
return self.episode(title)
120+
121+
122+
class Season(Video):
123+
TYPE = 'season'
124+
125+
def _loadData(self, data):
126+
super(Season, self)._loadData(data)
127+
self.librarySectionID = data.attrib.get('librarySectionID', NA)
128+
self.librarySectionTitle = data.attrib.get('librarySectionTitle', NA)
129+
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
130+
self.parentKey = data.attrib.get('parentKey', NA)
131+
self.parentTitle = data.attrib.get('parentTitle', NA)
132+
self.parentSummary = data.attrib.get('parentSummary', NA)
133+
self.index = data.attrib.get('index', NA)
134+
self.parentIndex = data.attrib.get('parentIndex', NA)
135+
self.parentThumb = data.attrib.get('parentThumb', NA)
136+
self.parentTheme = data.attrib.get('parentTheme', NA)
137+
self.leafCount = cast(int, data.attrib.get('leafCount', NA))
138+
self.viewedLeafCount = cast(int, data.attrib.get('viewedLeafCount', NA))
139+
140+
def episodes(self):
141+
childrenKey = '/library/metadata/%s/children' % self.ratingKey
142+
return list_items(self.server, childrenKey)
143+
144+
def episode(self, title):
145+
path = '/library/metadata/%s/children' % self.ratingKey
146+
return find_item(self.server, path, title)
147+
148+
def get(self, title):
149+
return self.episode(title)
150+
151+
def show(self):
152+
return list_items(self.server, self.parentKey)[0]
153+
154+
155+
class Episode(Video):
156+
TYPE = 'episode'
157+
158+
def _loadData(self, data):
159+
super(Episode, self)._loadData(data)
160+
self.librarySectionID = data.attrib.get('librarySectionID', NA)
161+
self.librarySectionTitle = data.attrib.get('librarySectionTitle', NA)
162+
self.grandparentKey = data.attrib.get('grandparentKey', NA)
163+
self.grandparentTitle = data.attrib.get('grandparentTitle', NA)
164+
self.grandparentThumb = data.attrib.get('grandparentThumb', NA)
165+
self.parentKey = data.attrib.get('parentKey', NA)
166+
self.parentIndex = data.attrib.get('parentIndex', NA)
167+
self.parentThumb = data.attrib.get('parentThumb', NA)
168+
self.contentRating = data.attrib.get('contentRating', NA)
169+
self.index = data.attrib.get('index', NA)
170+
self.rating = data.attrib.get('rating', NA)
171+
self.viewCount = cast(int, data.attrib.get('viewCount', 0))
172+
self.viewOffset = cast(int, data.attrib.get('viewOffset', 0))
173+
self.year = cast(int, data.attrib.get('year', NA))
174+
self.duration = cast(int, data.attrib.get('duration', NA))
175+
self.originallyAvailableAt = toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
176+
177+
def season(self):
178+
return list_items(self.server, self.parentKey)[0]
179+
180+
def show(self):
181+
return list_items(self.server, self.grandparentKey)[0]
182+
183+
184+
def build_item(server, elem, initpath):
185+
VIDEOCLS = {Movie.TYPE:Movie, Show.TYPE:Show, Season.TYPE:Season, Episode.TYPE:Episode}
186+
vtype = elem.attrib.get('type')
187+
if vtype in VIDEOCLS:
188+
cls = VIDEOCLS[vtype]
189+
return cls(server, elem, initpath)
190+
raise UnknownType('Unknown video type: %s' % vtype)
191+
192+
193+
def find_item(server, path, title):
194+
for elem in server.query(path):
195+
if elem.attrib.get('title').lower() == title.lower():
196+
return build_item(server, elem, path)
197+
raise NotFound('Unable to find title: %s' % title)
198+
199+
200+
def list_items(server, path, videotype=None):
201+
items = []
202+
for elem in server.query(path):
203+
if not videotype or elem.attrib.get('type') == videotype:
204+
try:
205+
items.append(build_item(server, elem, path))
206+
except UnknownType:
207+
pass
208+
return items
209+
210+
211+
def search_type(videotype):
212+
if videotype == Movie.TYPE: return 1
213+
elif videotype == Show.TYPE: return 2
214+
elif videotype == Season.TYPE: return 3
215+
elif videotype == Episode.TYPE: return 4
216+
raise NotFound('Unknown videotype: %s' % videotype)

‎requirements.pip

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#---------------------------------------------------------
2+
# PlexAPI Requirements
3+
#---------------------------------------------------------
4+
requests

‎setup.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/python
2+
"""
3+
Install PlexAPI
4+
"""
5+
from distutils.core import setup
6+
from setuptools import find_packages
7+
8+
# Fetch the current version
9+
with open('plexapi/__init__.py') as handle:
10+
for line in handle.readlines():
11+
if line.startswith('VERSION'):
12+
VERSION = line.split('=')[1].strip(" '\n")
13+
14+
setup(
15+
name='PlexAPI',
16+
version=VERSION,
17+
description='Python bindings for the Plex API.',
18+
author='Michael Shepanski',
19+
author_email='mjs7231@gmail.com',
20+
url='http://bitbucket.org/mjs7231/plexapi',
21+
packages=find_packages(),
22+
install_requires=['requests'],
23+
long_description=open('README.md').read(),
24+
keywords=['plex', 'api'],
25+
)

0 commit comments

Comments
 (0)
Please sign in to comment.