Compare commits

...

64 Commits

Author SHA1 Message Date
JonnyWong16
d9474cdcc5 Merge branch 'dev' 2016-06-11 11:40:24 -07:00
JonnyWong16
e49a34177a v1.4.6 2016-06-11 11:40:03 -07:00
JonnyWong16
67d203e011 Catch exception and attempt to reconnect websocket 2016-06-11 11:26:52 -07:00
JonnyWong16
0d38b3de16 Catch json parse error for current activity 2016-06-06 00:28:01 -07:00
JonnyWong16
38116a14f3 Fix double clear search button 2016-06-05 11:23:12 -07:00
JonnyWong16
b28f0b65f0 Add download logs button for Plex logs 2016-06-05 11:18:23 -07:00
JonnyWong16
13ab4a9363 Add log level filter to PlexPy logs 2016-06-05 11:17:58 -07:00
JonnyWong16
7cb7783a34 Make sure there is a date before trying to format it 2016-06-05 11:05:17 -07:00
JonnyWong16
d1a13dad38 Merge pull request #729 from sanderploegsma/feature/plex-log-level-filter
Add log level filter for plex server and media logs
2016-06-05 09:30:57 -07:00
JonnyWong16
b4e06dea99 Add some date options for notifications 2016-06-04 22:30:41 -07:00
JonnyWong16
0f92dc0fdf Add refresh button to images 2016-06-04 12:43:45 -07:00
JonnyWong16
6a58895d37 Reflect path suggested in installation guide (FreeNAS) 2016-06-04 08:48:58 -07:00
JonnyWong16
1709a2b7df Merge pull request #741 from Hellowlol/cachefix
Allow refresh of images
2016-06-04 08:43:51 -07:00
JonnyWong16
febb3da0c1 Merge pull request #735 from nortron/fix_freebsd_daemon_template_default_path
Reflect path suggested in installation guide (FreeBSD)
2016-06-04 08:43:29 -07:00
Hellowlol
552a428985 allow refresh of images
Allow refresh of images and fix bug where a disabled cache still would
use the cache
2016-06-02 17:27:23 +02:00
Tim Van
38e04bd42a Allow global config setting for PMS log window size. 2016-05-30 15:59:01 +02:00
JonnyWong16
8f0ba5ba4f Add notification agent ids to the API docs 2016-05-29 16:04:27 -07:00
JonnyWong16
c67aedceb1 Add library statistics to API 2016-05-28 14:09:59 -07:00
JonnyWong16
b3a7fbd9b5 Add pms token to loopback url when retrieving images 2016-05-28 14:02:21 -07:00
JonnyWong16
29522428de Catch exception when retrieving current activity 2016-05-28 14:01:29 -07:00
JonnyWong16
77bd52b2ae Add user details and stats to API 2016-05-26 19:25:14 -07:00
Nick N
d8112e7628 Reflect path suggested in installation guide 2016-05-26 11:44:03 -07:00
JonnyWong16
ffa208e73f Merge branch 'dev' 2016-05-25 21:34:15 -07:00
JonnyWong16
61ead15c38 v1.4.5 2016-05-25 21:33:50 -07:00
JonnyWong16
407e2ae481 Revert pms image proxy to use 127.0.0.1 2016-05-25 21:27:43 -07:00
JonnyWong16
5fb16edf43 Make sure library details were returned when joining shared libraries 2016-05-25 18:55:20 -07:00
JonnyWong16
8eb5c475bb Make sure port is a string when when matching pms url 2016-05-25 18:54:31 -07:00
JonnyWong16
84090310f7 Extract mapped IPv4 address from Plexivity import 2016-05-25 18:53:40 -07:00
JonnyWong16
fc98e2f052 Merge branch 'dev' 2016-05-24 22:17:22 -07:00
JonnyWong16
bedcfa9520 v1.4.4 2016-05-24 22:17:09 -07:00
JonnyWong16
bb152b590b Set all datatable tooltips to body container 2016-05-24 22:13:26 -07:00
JonnyWong16
3623732cf7 Sort sessions by session_key
* Try to minimize instances jumping around under homepage current
activity.
* Still need to fix for music activity. Some clients have a different
session_key per track.
2016-05-24 22:12:57 -07:00
JonnyWong16
05ba89f164 Make sure clip image urls are escaped 2016-05-24 21:33:37 -07:00
Sander Ploegsma
cb5053476d Add log level filter for plex server and media logs 2016-05-25 00:09:04 +02:00
JonnyWong16
cee656a053 Fix ip address in Plexivity import 2016-05-23 18:02:13 -07:00
JonnyWong16
cfc7d529e1 Merge branch 'dev' 2016-05-22 16:24:23 -07:00
JonnyWong16
a93dc68e6c v1.4.3 2016-05-22 16:24:01 -07:00
JonnyWong16
2d91cfd3db Fix basic auth 2016-05-22 16:22:35 -07:00
JonnyWong16
36e81f44cb Merge branch 'dev' 2016-05-22 15:04:25 -07:00
JonnyWong16
cb0e65337f v1.4.2 2016-05-22 15:04:17 -07:00
JonnyWong16
1c627f4649 Fix unable to save settings when checking http proxy 2016-05-22 14:54:44 -07:00
JonnyWong16
16cbfed20b Option to use HTTP basic authentication 2016-05-22 14:23:55 -07:00
JonnyWong16
f6a3bc57e2 Fix typos 2016-05-22 13:24:52 -07:00
JonnyWong16
594443d1dc Match port as well when retrieving pms url 2016-05-22 13:24:45 -07:00
JonnyWong16
c3378e1653 Merge branch 'dev' 2016-05-20 20:54:36 -07:00
JonnyWong16
bc57dd650c v1.4.1 2016-05-20 20:54:06 -07:00
JonnyWong16
311a8c6fa3 Try using requests for Join notifications 2016-05-20 20:44:16 -07:00
JonnyWong16
bdb43c0e9e Add paging for recently added to the API 2016-05-20 20:16:14 -07:00
JonnyWong16
8033b47596 Add secondary sort by most recent for watch statistics 2016-05-19 22:44:28 -07:00
JonnyWong16
9d5052cc68 Add http proxy checkbox to settings 2016-05-19 21:55:11 -07:00
JonnyWong16
f4c9dc8a5f Make sure cherrypy doesn't add the local port twice with http_proxy enabled 2016-05-19 21:54:58 -07:00
JonnyWong16
a3f0a78df0 Reduce cost factor for hashing passwords
* Also reduce memory cost
2016-05-19 20:24:22 -07:00
JonnyWong16
b70363e005 Fix resolution in stream data modal 2016-05-18 20:55:49 -07:00
JonnyWong16
65eab801e8 Format Join device ids 2016-05-18 20:55:49 -07:00
JonnyWong16
9e764248d3 Merge pull request #713 from Hellowlol/fix_log_order
Fix #705
2016-05-18 20:55:06 -07:00
Hellowlol
a660a1c44b fix https://github.com/drzoidberg33/plexpy/issues/705#issuecomment-219927893
Can you test and verify
2016-05-18 20:55:20 +02:00
JonnyWong16
33458c1bdb Make sure current activity returned sessions when refreshing 2016-05-17 21:10:05 -07:00
JonnyWong16
e5530182cd Make sure we get a result when trying to group the session 2016-05-17 20:58:28 -07:00
JonnyWong16
9ecabc3faf Just make sure redirects include http_root 2016-05-16 18:18:52 -07:00
JonnyWong16
8b58f6b861 Make sure pms_identifier is cleared first when verifying server 2016-05-16 09:14:00 -07:00
JonnyWong16
9e41bf529d Don't return inside the loop after sending XBMC/Plex notifications 2016-05-16 08:28:40 -07:00
JonnyWong16
36398fe958 Persist current activity artwork blur across refresh 2016-05-15 21:54:25 -07:00
JonnyWong16
69cfbea5f3 Refresh Join device list when changing API key 2016-05-15 17:20:28 -07:00
JonnyWong16
1e1e3beca6 Check for blank username/passwords on login 2016-05-15 17:20:14 -07:00
45 changed files with 1135 additions and 256 deletions

215
API.md
View File

@@ -543,6 +543,33 @@ Returns:
```
### get_library
Get a library's details.
```
Required parameters:
section_id (str): The id of the Plex library section
Optional parameters:
None
Returns:
json:
{"child_count": null,
"count": 887,
"do_notify": 1,
"do_notify_created": 1,
"keep_history": 1,
"library_art": "/:/resources/movie-fanart.jpg",
"library_thumb": "/:/resources/movie.png",
"parent_count": null,
"section_id": 1,
"section_name": "Movies",
"section_type": "movie"
}
```
### get_library_media_info
Get the data on the PlexPy media info tables.
@@ -619,6 +646,66 @@ Returns:
```
### get_library_user_stats
Get a library's user statistics.
```
Required parameters:
section_id (str): The id of the Plex library section
Optional parameters:
None
Returns:
json:
[{"friendly_name": "Jon Snow",
"total_plays": 170,
"user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar"
},
{"platform_type": "DanyKhaleesi69",
"total_plays": 42,
"user_id": 8008135,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar"
},
{...},
{...}
]
```
### get_library_watch_time_stats
Get a library's watch time statistics.
```
Required parameters:
section_id (str): The id of the Plex library section
Optional parameters:
None
Returns:
json:
[{"query_days": 1,
"total_plays": 0,
"total_time": 0
},
{"query_days": 7,
"total_plays": 3,
"total_time": 15694
},
{"query_days": 30,
"total_plays": 35,
"total_time": 63054
},
{"query_days": 0,
"total_plays": 508,
"total_time": 1183080
}
]
```
### get_logs
Get the PlexPy logs.
@@ -1061,6 +1148,7 @@ Required parameters:
count (str): Number of items to return
Optional parameters:
start (str): The item number to start at
section_id (str): The id of the Plex library section
Returns:
@@ -1310,6 +1398,35 @@ Returns:
```
### get_user
Get a user's details.
```
Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
Returns:
json:
{"allow_guest": 1,
"deleted_user": 0,
"do_notify": 1,
"email": "Jon.Snow.1337@CastleBlack.com",
"friendly_name": "Jon Snow",
"is_allow_sync": 1,
"is_home_user": 1,
"is_restricted": 0,
"keep_history": 1,
"shared_libraries": ["10", "1", "4", "5", "15", "20", "2"],
"user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
"username": "LordCommanderSnow"
}
```
### get_user_ips
Get the data on PlexPy users IP table.
@@ -1357,7 +1474,7 @@ Returns:
### get_user_logins
Get the data on PlexPy user login table.
Get the data on PlexPy user login table.
```
Required parameters:
@@ -1376,15 +1493,15 @@ Returns:
"recordsTotal": 2344,
"recordsFiltered": 10,
"data":
[{"browser": "Safari 7.0.3",
"friendly_name": "Jon Snow",
"host": "http://plexpy.castleblack.com",
"ip_address": "xxx.xxx.xxx.xxx",
"os": "Mac OS X",
"timestamp": 1462591869,
"user": "LordCommanderSnow",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
"user_group": "guest",
[{"browser": "Safari 7.0.3",
"friendly_name": "Jon Snow",
"host": "http://plexpy.castleblack.com",
"ip_address": "xxx.xxx.xxx.xxx",
"os": "Mac OS X",
"timestamp": 1462591869,
"user": "LordCommanderSnow",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
"user_group": "guest",
"user_id": 133788
},
{...},
@@ -1414,6 +1531,66 @@ Returns:
```
### get_user_player_stats
Get a user's player statistics.
```
Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
Returns:
json:
[{"platform_type": "Chrome",
"player_name": "Plex Web (Chrome)",
"result_id": 1,
"total_plays": 170
},
{"platform_type": "Chromecast",
"player_name": "Chromecast",
"result_id": 2,
"total_plays": 42
},
{...},
{...}
]
```
### get_user_watch_time_stats
Get a user's watch time statistics.
```
Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
Returns:
json:
[{"query_days": 1,
"total_plays": 0,
"total_time": 0
},
{"query_days": 7,
"total_plays": 3,
"total_time": 15694
},
{"query_days": 30,
"total_plays": 35,
"total_time": 63054
},
{"query_days": 0,
"total_plays": 508,
"total_time": 1183080
}
]
```
### get_users
Get a list of all users that have access to your server.
@@ -1519,6 +1696,24 @@ Send a notification using PlexPy.
```
Required parameters:
agent_id(str): The id of the notification agent to use
9 # Boxcar2
17 # Browser
10 # Email
16 # Facebook
0 # Growl
12 # IFTTT
18 # Join
4 # NotifyMyAndroid
3 # Plex Home Theater
1 # Prowl
5 # Pushalot
6 # Pushbullet
7 # Pushover
15 # Scripts
14 # Slack
13 # Telegram
11 # Twitter
2 # XBMC
subject(str): The subject of the message
body(str): The body of the message

View File

@@ -1,5 +1,66 @@
# Changelog
## v1.4.6 (2016-06-11)
* New: Added User and Library statistics to the API.
* New: Ability to refresh individual poster images without clearing the entire cache. (Thanks @Hellowlol)
* New: Added {added_date}, {updated_date}, and {last_viewed_date} to metadata notification options.
* New: Log level filter for Plex logs. (Thanks @sanderploegsma)
* New: Log level filter for PlexPy logs.
* New: Button to download Plex logs directly from the web interface.
* New: Advanced setting in the config file to change the number of Plex log lines retrieved.
* Fix: FreeBSD and FreeNAS init scripts to reflect the path in the installation guide. (Thanks @nortron)
* Fix: Monitoring crashing when failed to retrieve current activity.
## v1.4.5 (2016-05-25)
* Fix: PlexPy unable to start if failed to get shared libraries for a user.
* Fix: Matching port number when retrieving the PMS url.
* Fix: Extract mapped IPv4 address in Plexivity import.
* Change: Revert back to internal url when retrieving PMS images.
## v1.4.4 (2016-05-24)
* Fix: Image queries crashing the PMS when playing clips from channels.
* Fix: Plexivity import if IP address is missing.
* Fix: Tooltips shown behind the datatable headers.
* Fix: Current activity instances rendered in a random order causing them to jump around.
## v1.4.3 (2016-05-22)
* Fix: PlexPy not starting without any authentication method.
## v1.4.2 (2016-05-22)
* New: Option to use HTTP basic authentication instead of the HTML login form.
* Fix: Unable to save settings when enabling the HTTP proxy setting.
* Change: Match the PMS port when retrieving the PMS url.
## v1.4.1 (2016-05-20)
* New: HTTP Proxy checkbox in the settings. Enable this if using an SSL enabled reverse proxy in front of PlexPy.
* Fix: Check for blank username/password on login.
* Fix: Persist current activity artwork blur across refreshes when transcoding details are visible.
* Fix: Send notifications to multiple XBMC/Plex Home Theater devices.
* Fix: Reset PMS identifier when clicking verify server button in settings.
* Fix: Crash when trying to group current activity session in database.
* Fix: Check current activity returns sessions when refreshing.
* Fix: Logs sorted out of order.
* Fix: Resolution reported incorrectly in the stream info modal.
* Fix: PlexPy crashing when hashing password in the config file.
* Fix: CherryPy doubling the port number when accessing PlexPy locally with http_proxy enabled.
* Change: Sort by most recent for ties in watch statistics.
* Change: Refresh Join devices when changing the API key.
* Change: Format the Join device IDs.
* Change: Join notifications now sent with Python Requests module.
* Change: Add paging for recently added in the API.
## v1.4.0 (2016-05-15)
* New: An HTML form login page with sessions support.

View File

@@ -1,10 +1,10 @@
<!---
Reporting Issues:
* To ensure that a develpoer has enough information to work with please include all of the information below.
* To ensure that a developer has enough information to work with please include all of the information below.
Please provide as much detail as possible. Screenshots can be very useful to see the problem.
* Use proper markdown syntax to structure your post (i.e. code/log in code blocks).
See: https://help.github.com/articles/basic-writing-and-formatting-syntax/
* Iclude a link to your **FULL** log file that has the error(not just a few lines!).
* Include a link to your **FULL** log file that has the error(not just a few lines!).
Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
Feature Requests:

View File

@@ -214,6 +214,7 @@ def main():
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
webstart.initialize(web_config)

View File

@@ -1184,6 +1184,7 @@ a:hover .dashboard-recent-media-cover {
margin: 0 40px 0 25px;
height: 100px;
overflow: visible;
position: relative;
}
.summary-poster-face {
background-position: center;
@@ -1922,6 +1923,7 @@ a .library-user-instance-box:hover {
.home-platforms-instance-poster {
margin-left: 0px;
position: absolute;
overflow: hidden;
}
.home-platforms-instance-poster .home-platforms-poster-face {
background-position: center;
@@ -2079,6 +2081,7 @@ a .library-user-instance-box:hover {
.home-platforms-instance-list-poster {
position: absolute;
left: 20px;
overflow: hidden;
}
.home-platforms-instance-list-poster .home-platforms-list-poster-face {
background-position: center;
@@ -2954,4 +2957,51 @@ a.no-highlight:hover {
.datatable-wrap {
min-width: 150px;
max-width: 250px;
}
.inline-pre {
font-family: monospace;
margin: 0 2px;
padding: 2px 5px;
font-size: 13px;
color: #fff;
background-color: #555;
border: 0px solid #444;
border-radius: 3px;
}
.overlay-refresh-image {
opacity: 0;
color: #000;
font-size: 16px;
float: left;
position: absolute;
top: 0px;
right: 10px;
z-index: 1;
transition: all .1s cubic-bezier(.4,0,1,1);
-webkit-transition: all .1s cubic-bezier(.4,0,1,1);
-moz-transition: all .1s cubic-bezier(.4,0,1,1);
-o-transition: all .1s cubic-bezier(.4,0,1,1);
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
}
.overlay-refresh-image.left {
left: 10px;
}
.overlay-refresh-image.info-art {
color: #999;
top: 15px;
right: 25px;
opacity: 1;
text-shadow: none;
cursor: pointer;
}
.overlay-refresh-image.info-art:hover {
color: #fff;
text-shadow: none;
}
a:hover .overlay-refresh-image {
opacity: .25;
top: 8px;
}
a:hover .overlay-refresh-image:hover {
opacity: .9;
}

View File

@@ -106,6 +106,7 @@ DOCUMENTATION :: END
% else:
<div class="dashboard-activity-poster-face" style="background-image: url(${a['art']});"></div>
% endif
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
<div class="dashboard-activity-button-info">
<button type="button" class="btn btn-activity-info btn-lg" data-target="#stream-${a['session_key']}">
<i class="fa fa-info-circle"></i>

View File

@@ -61,6 +61,8 @@ DOCUMENTATION :: END
% if data is not None:
<%
from urllib import quote
from plexpy import helpers
data['indexes'] = helpers.cast_to_int(data['indexes'])
%>
@@ -71,7 +73,7 @@ DOCUMENTATION :: END
% else:
<a href="#">
% endif
<div class="dashboard-activity-poster">
<div class="dashboard-activity-poster" id="poster-${data['session_key']}">
% if not data['art'].startswith('interfaces') or not data['thumb'].startswith('interfaces'):
% if (data['media_type'] == 'movie' and not data['indexes']) or (data['indexes'] and not data['view_offset']):
<div id="bif-${data['session_key']}" class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${data['art']}&width=500&height=280&fallback=art);"></div>
@@ -90,9 +92,11 @@ DOCUMENTATION :: END
<div class="dashboard-activity-poster-face" style="background-image: url(${data['thumb']});"></div>
% else:
% if data['art']:
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${data['art']}&width=500&height=280&fallback=art);"></div>
<!--Hacky solution to escape the image url until I come up with something better-->
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${quote(data['art'])}&width=500&height=280&fallback=art);"></div>
% else:
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);"></div>
<!--Hacky solution to escape the image url until I come up with something better-->
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${quote(data['thumb'])}&width=500&height=280&fallback=art);"></div>
% endif
% endif
% elif data['media_type'] == 'photo':
@@ -104,8 +108,9 @@ DOCUMENTATION :: END
% else:
<div class="dashboard-activity-poster-face" style="background-image: url(${data['art']});"></div>
% endif
<span class="overlay-refresh-image left" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
<div class="dashboard-activity-button-info">
<button type="button" class="btn btn-activity-info btn-lg" data-target="#stream-${data['session_key']}">
<button type="button" class="btn btn-activity-info btn-lg" data-target="#stream-${data['session_key']}" data-id="${data['session_key']}">
<i class="fa fa-info-circle"></i>
</button>
</div>

View File

@@ -103,6 +103,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['grandparent_thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -149,6 +150,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['grandparent_thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">
@@ -199,6 +201,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['grandparent_thumb'] != '':
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -241,6 +244,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['grandparent_thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">
@@ -295,6 +299,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -341,6 +346,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">
@@ -391,6 +397,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -433,6 +440,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">
@@ -487,6 +495,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['grandparent_thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -533,6 +542,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['grandparent_thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">
@@ -583,6 +593,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['grandparent_thumb'] != '':
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -625,6 +636,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['grandparent_thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">
@@ -847,6 +859,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][0]['thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-poster">
@@ -903,6 +916,7 @@ DOCUMENTATION :: END
% if top_stat['rows'][loop.index]['thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
% else:
<div class="home-platforms-instance-list-poster">

View File

@@ -103,10 +103,25 @@
type: 'GET',
cache: false,
async: true,
error: function (xhr, status, error) {
console.log(status + ': ' + error);
},
complete: function (xhr, status) {
$('#dashboard-checking-activity').remove();
var current_activity = $.parseJSON(xhr.responseText);
var current_activity;
try {
current_activity = $.parseJSON(xhr.responseText);
} catch (e) {
console.log(status + ': ' + e);
current_activity = null;
}
if (!(current_activity)) {
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.</div>');
return
}
var stream_count = parseInt(current_activity.stream_count);
var sessions = current_activity.sessions;
@@ -150,6 +165,7 @@
bif_poster.animate({ opacity: 0 }, { duration: 1000, queue: false });
bif_poster.after($('<div id="bif-' + key + '"class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img='
+ s.bif_thumb + '&width=500&height=280&fallback=art);"></div>').fadeIn(1000, function () { bif_poster.remove() }));
blurArtwork(key);
}
// if transcoding, update the transcode state
@@ -210,14 +226,18 @@
getCurrentActivity();
}, 15000);
function blurArtwork(session_key) {
var filterVal = $('#stream-' + session_key).is(':visible') ? 'blur(5px)' : '';
$($('#poster-' + session_key).find('.dashboard-activity-poster-face, .dashboard-activity-cover-face'))
.css('filter', filterVal).css('webkitFilter', filterVal).css('mozFilter', filterVal).css('oFilter', filterVal).css('msFilter', filterVal);
}
// Show/Hide activity info
$('#currentActivity').on('click', '.btn-activity-info', function (e) {
e.preventDefault();
$($(this).attr('data-target')).toggle();
var id = $(this).closest('.dashboard-instance').data('id');
var filterVal = $('#stream-' + id).is(':visible') ? 'blur(5px)' : '';
$($(this).closest('.dashboard-activity-poster').find('.dashboard-activity-poster-face, .dashboard-activity-cover-face'))
.css('filter',filterVal).css('webkitFilter',filterVal).css('mozFilter',filterVal).css('oFilter',filterVal).css('msFilter',filterVal);
var key = $(this).data('id');
blurArtwork(key);
});
// Add hover class to dashboard-instance

View File

@@ -68,6 +68,7 @@ DOCUMENTATION :: END
<div class="container-fluid">
<div class="row">
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
<div class="summary-container">
<div class="summary-navbar">
<div class="col-md-12">
@@ -119,18 +120,21 @@ DOCUMENTATION :: END
<span></span>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=cover);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% else:
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</a>
</div>

View File

@@ -51,6 +51,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
% elif data['children_type'] == 'episode':
@@ -63,6 +64,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="item-children-instance-text-wrapper episode-item">
@@ -74,6 +76,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="item-children-instance-text-wrapper album-item">

View File

@@ -65,6 +65,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
<div class="item-children-instance-text-wrapper season-item">
<h3 title="${child['title']}">${child['title']}</h3>
@@ -87,6 +88,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
<div class="item-children-instance-text-wrapper season-item">
<h3 title="${child['title']}">${child['title']}</h3>
@@ -109,6 +111,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
<div class="item-children-instance-text-wrapper season-item">
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
@@ -131,6 +134,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
<div class="item-children-instance-text-wrapper episode-item">
<h3 title="${child['grandparent_title']}">${child['grandparent_title']}</h3>
@@ -154,6 +158,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
<div class="item-children-instance-text-wrapper album-item">
<h3 title="${child['title']}">${child['title']}</h3>
@@ -175,6 +180,7 @@ DOCUMENTATION :: END
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
<div class="item-children-instance-text-wrapper album-item">
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
@@ -204,6 +210,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
<div class="item-children-instance-text-wrapper album-item">
<h3 title="${child['grandparent_title']}">${child['grandparent_title']}</h3>
<h3 title="${child['title']}">${child['title']}</h3>

View File

@@ -399,4 +399,27 @@ window.onerror = function (message, file, line) {
'line': line
};
$.post("log_js_errors", e, function (data) { });
};
};
$('*').on('click', '.refresh_pms_image', function (e) {
e.preventDefault();
e.stopPropagation();
var background_div = $(this).parent().siblings(['style*=pms_image_proxy']).first();
var pms_proxy_url = background_div.css('background-image');
pms_proxy_url = /^url\((['"]?)(.*)\1\)$/.exec(pms_proxy_url);
pms_proxy_url = pms_proxy_url ? pms_proxy_url[2] : ""; // If matched, retrieve url, otherwise ""
if (pms_proxy_url.indexOf('pms_image_proxy') == -1) {
console.log('PMS image proxy url not found.');
} else {
if (pms_proxy_url.indexOf('refresh=true') > -1) {
pms_proxy_url = pms_proxy_url.replace("&refresh=true", "");
console.log(pms_proxy_url)
background_div.css('background-image', 'url(' + pms_proxy_url + ')');
background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)');
} else {
background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)');
}
}
});

View File

@@ -448,10 +448,10 @@ function childTableOptions(rowData) {
// Create the tooltips.
$('.expand-history-tooltip').tooltip({ container: 'body' });
$('.external-ip-tooltip').tooltip();
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
$('.external-ip-tooltip').tooltip({ container: 'body' });
$('.transcode-tooltip').tooltip({ container: 'body' });
$('.media-type-tooltip').tooltip({ container: 'body' });
$('.watched-tooltip').tooltip({ container: 'body' });
$('.thumb-tooltip').popover({
html: true,
container: 'body',

View File

@@ -132,8 +132,8 @@ history_table_modal_options = {
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.transcode-tooltip').tooltip({ container: 'body' });
$('.media-type-tooltip').tooltip({ container: 'body' });
$('.thumb-tooltip').popover({
html: true,
container: '#history-modal',

View File

@@ -217,10 +217,10 @@ libraries_list_table_options = {
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.purge-tooltip').tooltip();
$('.edit-tooltip').tooltip();
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.purge-tooltip').tooltip({ container: 'body' });
$('.edit-tooltip').tooltip({ container: 'body' });
$('.transcode-tooltip').tooltip({ container: 'body' });
$('.media-type-tooltip').tooltip({ container: 'body' });
$('.thumb-tooltip').popover({
html: true,
container: 'body',

View File

@@ -220,13 +220,14 @@ users_list_table_options = {
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.purge-tooltip').tooltip();
$('.edit-tooltip').tooltip();
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
$('.purge-tooltip').tooltip({ container: 'body' });
$('.edit-tooltip').tooltip({ container: 'body' });
$('.transcode-tooltip').tooltip({ container: 'body' });
$('.media-type-tooltip').tooltip({ container: 'body' });
$('.watched-tooltip').tooltip({ container: 'body' });
$('.thumb-tooltip').popover({
html: true,
container: 'body',
trigger: 'hover',
placement: 'right',
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',

View File

@@ -39,6 +39,7 @@ DOCUMENTATION :: END
<div class="row">
% if data['library_art']:
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['library_art']}&width=1920&height=1080)"></div>
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
<div class="summary-container">
<div class="summary-navbar">
@@ -362,7 +363,7 @@ DOCUMENTATION :: END
// Populate watch time stats
$.ajax({
url: 'get_library_watch_time_stats',
url: 'library_watch_time_stats',
async: true,
data: { section_id: section_id },
complete: function(xhr, status) {
@@ -372,7 +373,7 @@ DOCUMENTATION :: END
// Populate user stats
$.ajax({
url: 'get_library_user_stats',
url: 'library_user_stats',
async: true,
data: { section_id: section_id },
complete: function(xhr, status) {
@@ -498,7 +499,7 @@ DOCUMENTATION :: END
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'get_library_recently_watched',
url: 'library_recently_watched',
async: true,
data: {
section_id: section_id,
@@ -514,7 +515,7 @@ DOCUMENTATION :: END
function recentlyAdded() {
// Populate recently added
$.ajax({
url: 'get_library_recently_added',
url: 'library_recently_added',
async: true,
data: {
section_id: section_id,

View File

@@ -60,6 +60,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="dashboard-recent-media-metacontainer">

View File

@@ -21,7 +21,33 @@
<span><i class="fa fa-list-alt"></i> Logs</span>
</div>
<div class="button-bar">
<button class="btn btn-dark" id="download-plexpylog"><i class="fa fa-download"></i> Download log</button>
<div class="btn-group" id="plexpy-log-levels">
<label>
<select name="plexpy-log-level-filter" id="plexpy-log-level-filter" class="btn" style="color: inherit;">
<option value="">All log levels</option>
<option disabled>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</option>
<option value="DEBUG">Debug</option>
<option value="INFO">Info</option>
<option value="WARN">Warning</option>
<option value="ERROR">Error</option>
</select>
</label>
</div>
<div class="btn-group" id="plex-log-levels" style="display: none;">
<label>
<select name="plex-log-level-filter" id="plex-log-level-filter" class="btn" style="color: inherit;">
<option value="">All log levels</option>
<option disabled>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</option>
<option value="DEBUG">Debug</option>
<option value="INFO">Info</option>
<option value="WARN">Warning</option>
<option value="ERROR">Error</option>
</select>
</label>
</div>
<button class="btn btn-dark" id="download-plexpylog"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-plexserverlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-plexscannerlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="clear-logs"><i class="fa fa-trash-o"></i> Clear logs</button>
<button class="btn btn-dark" id="clear-notify-logs" style="display: none;"><i class="fa fa-trash-o"></i> Clear logs</button>
<button class="btn btn-dark" id="clear-login-logs" style="display: none;"><i class="fa fa-trash-o"></i> Clear logs</button>
@@ -40,27 +66,25 @@
<div role="tabpanel" class="tab-pane active" id="tabs-1">
<table class="display" id="log_table" width="100%">
<thead>
<tr>
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
<th class="desktop" align="left" id="level">Level</th>
<th class="all" align="left" id="message">Message</th>
</tr>
<tr>
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
<th class="desktop" align="left" id="level">Level</th>
<th class="all" align="left" id="message">Message</th>
</tr>
</thead>
<tbody>
</tbody>
<tbody></tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-2">
<table class="display" id="plex_log_table" width="100%">
<thead>
<tr>
<th align="left" id="plex_timestamp">Timestamp</th>
<th align="left" id="plex_level">Level</th>
<th align="left" id="plex_message">Message</th>
</tr>
<tr>
<th align="left" id="plex_timestamp">Timestamp</th>
<th align="left" id="plex_level">Level</th>
<th align="left" id="plex_message">Message</th>
</tr>
</thead>
<tbody>
</tbody>
<tbody></tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-3">
@@ -114,7 +138,8 @@
</div>
<br>
<div align="center">Refresh rate:
<div align="center">
Refresh rate:
<select id="refreshrate" onchange="setRefresh()">
<option value="0" selected="selected">No Refresh</option>
<option value="5">5 Seconds</option>
@@ -139,21 +164,62 @@
<script>
$(document).ready(function() {
loadPlexPyLogs();
loadPlexPyLogs(selected_log_level);
clearSearchButton('log_table', log_table);
});
function loadPlexPyLogs() {
var log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
function bindLogLevelFilter() {
clearLogLevelFilter();
var log_level_column = this.api().column(1);
var select = $('#plex-log-level-filter');
select.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex(
$(this).val()
);
var search_string = '';
var levelIndex = log_levels.indexOf(val);
if (levelIndex >= 0) {
search_string = '^' + log_levels
.slice(levelIndex)
.join('|') + '$';
}
log_level_column
.search(search_string, true, false)
.draw();
}).change();
}
function clearLogLevelFilter() {
$('#plex-log-level-filter').off('change');
}
var selected_log_level = null;
function loadPlexPyLogs(selected_log_level) {
log_table_options.ajax = {
url: "getLog"
url: "getLog",
type: 'post',
data: function (d) {
return {
json_data: JSON.stringify(d),
log_level: selected_log_level
};
}
}
log_table = $('#log_table').DataTable(log_table_options);
$('#plexpy-log-level-filter').on('change', function () {
selected_log_level = $(this).val() || null;
log_table.draw();
});
}
function loadPlexLogs() {
plex_log_table_options.ajax = {
url: "get_plex_log?log_type=server"
}
plex_log_table_options.initComplete = bindLogLevelFilter;
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
}
@@ -161,6 +227,7 @@
plex_log_table_options.ajax = {
url: "get_plex_log?log_type=scanner"
}
plex_log_table_options.initComplete = bindLogLevelFilter;
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
}
@@ -190,17 +257,25 @@
}
$("#plexpy-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs();
loadPlexPyLogs(selected_log_level);
clearSearchButton('log_table', log_table);
});
$("#plex-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#plex-log-levels").show();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").show()
$("#download-plexscannerlog").hide()
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexLogs();
@@ -208,8 +283,12 @@
});
$("#plex-scanner-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#plex-log-levels").show();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").show()
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexScannerLogs();
@@ -217,8 +296,12 @@
});
$("#notification-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#clear-notify-logs").show();
$("#clear-login-logs").hide();
loadNotificationLogs();
@@ -226,8 +309,12 @@
});
$("#login-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#clear-notify-logs").hide();
$("#clear-login-logs").show();
loadLoginLogs();
@@ -263,6 +350,13 @@
window.location.href = "download_log";
});
$("#download-plexserverlog").click(function () {
window.location.href = "download_plex_log?log_type=server";
});
$("#download-plexscannerlog").click(function () {
window.location.href = "download_plex_log?log_type=scanner";
});
$("#clear-notify-logs").click(function () {
$("#confirm-message").text("Are you sure you want to clear the PlexPy notification logs?");

View File

@@ -217,7 +217,7 @@
}
}
$('#pushbullet_apikey, #pushover_apitoken, #scripts_folder').on('change', function () {
$('#pushbullet_apikey, #pushover_apitoken, #scripts_folder, #join_apikey').on('change', function () {
// Reload modal to update certain fields
doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal);
return false;

View File

@@ -49,6 +49,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="dashboard-recent-media-metacontainer">
@@ -69,6 +70,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="dashboard-recent-media-metacontainer">
@@ -91,6 +93,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="dashboard-recent-media-metacontainer">
@@ -109,6 +112,5 @@ DOCUMENTATION :: END
</div>
</div>
% else:
<div class="text-muted">There was an error communicating with your Plex Server. Please check your <a href="settings">settings</a>.
</div><br>
<div class="text-muted">There was an error communicating with your Plex Server.</div><br>
% endif

View File

@@ -432,6 +432,13 @@
</div>
<p class="help-block">The base URL of the web server. Used for reverse proxies.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" class="http-settings" name="http_proxy" id="http_proxy" value="1" ${config['http_proxy']}> Enable HTTP Proxy
</label>
<p class="help-block">Respect the X-Forwarded-Proto header. Used for reverse proxies with SSL.</p>
</div>
<br />
<div class="checkbox">
<label>
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" ${config['launch_browser']}> Launch Browser on Startup
@@ -521,10 +528,17 @@
<label>
<input type="checkbox" name="http_hash_password" id="http_hash_password" value="1" ${config['http_hash_password']} data-parsley-trigger="change"> Hash Password in the Config File
</label>
<span id="hashPasswordCheck" style="color: #eb8600; padding-left: 10px;"></span>
<p class="help-block">Store a hashed password in the config file.<br />Warning: Your password cannot be recovered if forgotten!</p>
</div>
<input type="text" id="http_hashed_password" name="http_hashed_password" value="${config['http_hashed_password']}" style="display: none;" data-parsley-trigger="change" data-parsley-type="integer" data-parsley-range="[0, 1]"
data-parsley-errors-container="#http_hash_password_error" data-parsley-error-message="Cannot un-hash password, please set a new password." data-parsley-no-focus required>
<div class="checkbox">
<label>
<input type="checkbox" class="auth-settings" name="http_basic_auth" id="http_basic_auth" value="1" ${config['http_basic_auth']} data-parsley-trigger="change"> Use Basic Authentication
</label>
<p class="help-block">Use basic HTTP authentication instead of the HTML login form.</p>
</div>
<div class="padded-header">
@@ -559,7 +573,7 @@
</div>
</div>
</div>
<p class="help-block">Current API key: <strong><br/>${config['api_key']}</strong></p>
<p class="help-block">Current API key: <strong> ${config['api_key']}</strong></p>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
@@ -1811,6 +1825,26 @@
<td><strong>{year}</strong></td>
<td>The release year for the item.</td>
</tr>
<tr>
<td><strong>{release_date}</strong></td>
<td>The release date (in date format) for the item.</td>
</tr>
<tr>
<td><strong>{air_date}</strong></td>
<td>The air date (in date format) for the item.</td>
</tr>
<tr>
<td><strong>{added_date}</strong></td>
<td>The date (in date format) the item was added to Plex.</td>
</tr>
<tr>
<td><strong>{updated_date}</strong></td>
<td>The date (in date format) the item was updated on Plex.</td>
</tr>
<tr>
<td><strong>{last_viewed_date}</strong></td>
<td>The date (in date format) the item was last viewed on Plex.</td>
</tr>
<tr>
<td><strong>{studio}</strong></td>
<td>The studio for the item.</td>
@@ -2234,7 +2268,6 @@ $(document).ready(function() {
$( ".pms-settings" ).change(function() {
serverChanged = true;
$("#pms_identifier").val("");
$("#pms-verify-status").html("");
$("#server_changed").prop('checked', true);
verifyServer();
});
@@ -2287,6 +2320,7 @@ $(document).ready(function() {
}
$('#verify_server_button').on('click', function(){
$("#pms_identifier").val("");
verifyServer();
});
@@ -2563,7 +2597,11 @@ $(document).ready(function() {
});
function allowGuestAccessCheck () {
if ($('#http_username').val() == '' || $('#http_password').val() == '') {
if ($("#http_basic_auth").is(":checked")) {
$("#allow_guest_access").attr("disabled", true);
$("#allow_guest_access").attr("checked", false);
$("#allowGuestCheck").html("Guest access cannot be enabled with basic authentication.");
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
$("#allow_guest_access").attr("disabled", true);
$("#allow_guest_access").attr("checked", false);
$("#allowGuestCheck").html("You must set an admin password above to allow guest access.");
@@ -2574,18 +2612,30 @@ $(document).ready(function() {
}
allowGuestAccessCheck();
$('#http_username, #http_password').change(function () {
$('#http_username, #http_password, #http_basic_auth').change(function () {
allowGuestAccessCheck();
});
$("#http_hash_password").click(function(){
function hashPasswordCheck () {
if ($("#http_basic_auth").is(":checked")) {
$("#http_hash_password").attr("checked", false);
$("#http_hash_password").attr("disabled", true);
$("#hashPasswordCheck").html("Password cannot be hashed with basic authentication.");
} else {
$("#http_hash_password").attr("disabled", false);
$("#hashPasswordCheck").html("");
}
if (!($("#http_hash_password").is(":checked")) && $("#http_hashed_password").val() == "1" && $("#http_password").val() == " ") {
$("#http_hashed_password").val(-1);
} else if ($("#http_hash_password").is(":checked") && $("#http_hashed_password").val() == "-1" && $("#http_password").val() == " ") {
$("#http_hashed_password").val(1);
$("#http_hash_password_error").html("");
}
}
hashPasswordCheck();
$('#http_password, #http_hash_password, #http_basic_auth').change(function () {
hashPasswordCheck();
});
$('#http_password').change(function () {
@@ -2594,4 +2644,4 @@ $(document).ready(function() {
});
});
</script>
</%def>
</%def>

View File

@@ -59,7 +59,7 @@ DOCUMENTATION :: END
<ul class="list-unstyled">
<li>Container: <strong>${data['transcode_container'] if data['transcode_container'] else data['container']}</strong></li>
% if data['media_type'] != 'track':
<li>Resolution: <strong>${data['video_resolution'] + 'p' if data['video_resolution'] != 'sd' else data['video_resolution']}</strong></li>
<li>Resolution: <strong>${data['transcode_height'] if data['transcode_height'] else data['height']}p</strong></li>
% endif
</ul>
</div>
@@ -101,7 +101,7 @@ DOCUMENTATION :: END
<ul class="list-unstyled">
<li>Container: <strong>${data['container']}</strong></li>
% if data['media_type'] != 'track':
<li>Resolution: <strong>${data['height']}p</strong></li>
<li>Resolution: <strong>${data['video_resolution'] + 'p' if data['video_resolution'] != 'sd' else data['video_resolution']}</strong></li>
% endif
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
</ul>

View File

@@ -383,7 +383,7 @@ DOCUMENTATION :: END
// Populate watch time stats
$.ajax({
url: 'get_user_watch_time_stats',
url: 'user_watch_time_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
@@ -393,7 +393,7 @@ DOCUMENTATION :: END
// Populate platform stats
$.ajax({
url: 'get_user_player_stats',
url: 'user_player_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {

View File

@@ -49,6 +49,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
</div>
</a>
<div class="dashboard-recent-media-metacontainer">

View File

@@ -14,7 +14,7 @@
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/plexpy
# Default: /usr/local/share/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.
@@ -30,7 +30,7 @@ load_rc_config ${name}
: ${plexpy_enable:="NO"}
: ${plexpy_user:="_sabnzbd"}
: ${plexpy_dir:="/usr/local/plexpy"}
: ${plexpy_dir:="/usr/local/share/plexpy"}
: ${plexpy_chdir:="${plexpy_dir}"}
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
: ${plexpy_flags:=""}

View File

@@ -14,7 +14,7 @@
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/plexpy
# Default: /usr/local/share/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.

View File

@@ -195,7 +195,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
if not base:
base = request.headers.get('Host', '127.0.0.1')
port = request.local.port
if port != 80:
if port != 80 and not base.endswith(':%s' % port):
base += ':%s' % port
if base.find("://") == -1:

View File

@@ -32,7 +32,7 @@ HASH_FUNCTION = 'sha256' # Must be in hashlib.
# Linear to the hashing time. Adjust to be high but take a reasonable
# amount of time on your server. Measure with:
# python -m timeit -s 'import passwords as p' 'p.make_hash("something")'
COST_FACTOR = 29000
COST_FACTOR = 10000
def make_hash(password):

View File

@@ -72,7 +72,7 @@ def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None):
rv = u = _pseudorandom(salt + _pack_int(block))
for i in xrange(iterations - 1):
u = _pseudorandom(''.join(map(chr, u)))
rv = starmap(xor, izip(rv, u))
rv = list(starmap(xor, izip(rv, u)))
buf.extend(rv)
return ''.join(map(chr, buf))[:keylen]

View File

@@ -48,9 +48,10 @@ class ActivityHandler(object):
pms_connect = pmsconnect.PmsConnect()
session_list = pms_connect.get_current_activity()
for session in session_list['sessions']:
if int(session['session_key']) == self.get_session_key():
return session
if session_list:
for session in session_list['sessions']:
if int(session['session_key']) == self.get_session_key():
return session
return None

View File

@@ -219,26 +219,30 @@ class ActivityProcessor(object):
args = [session['user_id']]
result = self.db.select(query=query, args=args)
new_session = {'id': result[0]['id'],
'rating_key': result[0]['rating_key'],
'view_offset': result[0]['view_offset'],
'user_id': result[0]['user_id'],
'reference_id': result[0]['reference_id']}
if len(result) == 1:
prev_session = None
else:
new_session = prev_session = last_id = None
if len(result) > 1:
new_session = {'id': result[0]['id'],
'rating_key': result[0]['rating_key'],
'view_offset': result[0]['view_offset'],
'user_id': result[0]['user_id'],
'reference_id': result[0]['reference_id']}
prev_session = {'id': result[1]['id'],
'rating_key': result[1]['rating_key'],
'view_offset': result[1]['view_offset'],
'user_id': result[1]['user_id'],
'reference_id': result[1]['reference_id']}
else:
# Get the last insert row id
result = self.db.select(query='SELECT last_insert_rowid() AS last_id')
last_id = result[0]['last_id'] if result else None
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key'] \
and prev_session['view_offset'] <= new_session['view_offset']):
if prev_session == new_session == None:
args = [last_id, last_id]
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
args = [prev_session['reference_id'], new_session['id']]
else:
args = [new_session['id'], new_session['id']]

View File

@@ -46,6 +46,7 @@ _CONFIG_DEFINITIONS = {
'PMS_IP': (str, 'PMS', '127.0.0.1'),
'PMS_IS_REMOTE': (int, 'PMS', 0),
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000),
'PMS_NAME': (unicode, 'PMS', ''),
'PMS_PORT': (int, 'PMS', 32400),
'PMS_TOKEN': (str, 'PMS', ''),
@@ -185,6 +186,7 @@ _CONFIG_DEFINITIONS = {
'HTTPS_KEY': (str, 'General', ''),
'HTTPS_DOMAIN': (str, 'General', 'localhost'),
'HTTPS_IP': (str, 'General', '127.0.0.1'),
'HTTP_BASIC_AUTH': (int, 'General', 0),
'HTTP_ENVIRONMENT': (str, 'General', 'production'),
'HTTP_HASH_PASSWORD': (int, 'General', 0),
'HTTP_HASHED_PASSWORD': (int, 'General', 0),

View File

@@ -198,7 +198,7 @@ class DataFactory(object):
top_tv = []
try:
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
't.media_type, t.content_rating, t.labels, ' \
't.media_type, t.content_rating, t.labels, t.started, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
@@ -210,7 +210,7 @@ class DataFactory(object):
' AND session_history.media_type = "episode" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \
'ORDER BY %s DESC ' \
'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -246,7 +246,7 @@ class DataFactory(object):
popular_tv = []
try:
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
't.media_type, t.content_rating, t.labels, ' \
't.media_type, t.content_rating, t.labels, t.started, ' \
'COUNT(DISTINCT t.user_id) AS users_watched, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) as total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
@@ -259,7 +259,7 @@ class DataFactory(object):
' AND session_history.media_type = "episode" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \
'ORDER BY users_watched DESC, %s DESC ' \
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -293,7 +293,7 @@ class DataFactory(object):
top_movies = []
try:
query = 'SELECT t.id, t.full_title, t.rating_key, t.thumb, t.section_id, ' \
't.media_type, t.content_rating, t.labels, ' \
't.media_type, t.content_rating, t.labels, t.started, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
@@ -305,7 +305,7 @@ class DataFactory(object):
' AND session_history.media_type = "movie" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.full_title ' \
'ORDER BY %s DESC ' \
'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -341,7 +341,7 @@ class DataFactory(object):
popular_movies = []
try:
query = 'SELECT t.id, t.full_title, t.rating_key, t.thumb, t.section_id, ' \
't.media_type, t.content_rating, t.labels, ' \
't.media_type, t.content_rating, t.labels, t.started, ' \
'COUNT(DISTINCT t.user_id) AS users_watched, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) as total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
@@ -354,7 +354,7 @@ class DataFactory(object):
' AND session_history.media_type = "movie" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.full_title ' \
'ORDER BY users_watched DESC, %s DESC ' \
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -388,7 +388,7 @@ class DataFactory(object):
top_music = []
try:
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
't.media_type, t.content_rating, t.labels, ' \
't.media_type, t.content_rating, t.labels, t.started, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
@@ -400,7 +400,7 @@ class DataFactory(object):
' AND session_history.media_type = "track" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \
'ORDER BY %s DESC ' \
'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -436,7 +436,7 @@ class DataFactory(object):
popular_music = []
try:
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
't.media_type, t.content_rating, t.labels, ' \
't.media_type, t.content_rating, t.labels, t.started, ' \
'COUNT(DISTINCT t.user_id) AS users_watched, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) as total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
@@ -449,7 +449,7 @@ class DataFactory(object):
' AND session_history.media_type = "track" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \
'ORDER BY users_watched DESC, %s DESC ' \
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -482,7 +482,7 @@ class DataFactory(object):
elif stat == 'top_users':
top_users = []
try:
query = 'SELECT t.user, t.user_id, t.user_thumb, t.custom_thumb, ' \
query = 'SELECT t.user, t.user_id, t.user_thumb, t.custom_thumb, t.started, ' \
'(CASE WHEN t.friendly_name IS NULL THEN t.username ELSE t.friendly_name END) ' \
' AS friendly_name, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
@@ -496,7 +496,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.user_id ' \
'ORDER BY %s DESC ' \
'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:
@@ -536,7 +536,7 @@ class DataFactory(object):
top_platform = []
try:
query = 'SELECT t.platform, ' \
query = 'SELECT t.platform, t.started, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
@@ -547,7 +547,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.platform ' \
'ORDER BY %s DESC ' \
'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
except Exception as e:

View File

@@ -727,6 +727,16 @@ def build_notify_text(session=None, timeline=None, notify_action=None, agent_id=
'track_num': metadata['media_index'].zfill(1),
'track_num00': metadata['media_index'].zfill(2),
'year': metadata['year'],
'release_date': arrow.get(metadata['originally_available_at']).format(date_format)
if metadata['originally_available_at'] else '',
'air_date': arrow.get(metadata['originally_available_at']).format(date_format)
if metadata['originally_available_at'] else '',
'added_date': arrow.get(metadata['added_at']).format(date_format)
if metadata['added_at'] else '',
'updated_date': arrow.get(metadata['updated_at']).format(date_format)
if metadata['updated_at'] else '',
'last_viewed_date': arrow.get(metadata['last_viewed_at']).format(date_format)
if metadata['last_viewed_at'] else '',
'studio': metadata['studio'],
'content_rating': metadata['content_rating'],
'directors': ', '.join(metadata['directors']),

View File

@@ -786,12 +786,13 @@ class XBMC(object):
raise Exception
else:
logger.info(u"PlexPy Notifiers :: XBMC notification sent.")
return True
except Exception:
logger.warn(u"PlexPy Notifiers :: XBMC notification filed.")
logger.warn(u"PlexPy Notifiers :: XBMC notification failed.")
return False
return True
def return_config_options(self):
config_option = [{'label': 'XBMC Host:Port',
'value': self.hosts,
@@ -870,11 +871,12 @@ class Plex(object):
raise Exception
else:
logger.info(u"PlexPy Notifiers :: Plex Home Theater notification sent.")
return True
except Exception:
logger.warn(u"PlexPy Notifiers :: Plex Home Theater notification filed.")
logger.warn(u"PlexPy Notifiers :: Plex Home Theater notification failed.")
return False
return True
def return_config_options(self):
config_option = [{'label': 'Plex Home Theater Host:Port',
@@ -2567,18 +2569,12 @@ class JOIN(object):
'title': subject.encode("utf-8"),
'text': message.encode("utf-8")}
http_handler = HTTPSConnection("joinjoaomgcd.appspot.com")
http_handler.request("POST",
"/_ah/api/messaging/v1/sendPush?%s" % urlencode(data))
response = http_handler.getresponse()
request_status = response.status
# logger.debug(u"PushBullet response status: %r" % request_status)
# logger.debug(u"PushBullet response headers: %r" % response.getheaders())
# logger.debug(u"PushBullet response body: %r" % response.read())
response = requests.post('https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush',
params=data)
request_status = response.status_code
if request_status == 200:
data = json.loads(response.read())
data = json.loads(response.text)
if data.get('success'):
logger.info(u"PlexPy Notifiers :: Join notification sent.")
return True
@@ -2632,7 +2628,10 @@ class JOIN(object):
return {'': ''}
def return_config_options(self):
devices = '<br>'.join(['%s: %s' % (v, k) for k, v in self.get_devices().iteritems() if k])
devices = '<br>'.join(['%s: <span class="inline-pre">%s</span>'
% (v, k) for k, v in self.get_devices().iteritems() if k])
if not devices:
devices = 'Enter your Join API key to load your device list.'
config_option = [{'label': 'Join API Key',
'value': self.apikey,

View File

@@ -100,6 +100,7 @@ def extract_plexivity_xml(xml=None):
video_resolution = helpers.get_xml_attr(c, 'videoResolution')
width = helpers.get_xml_attr(c, 'width')
ip_address = ''
machine_id = ''
platform = ''
player = ''
@@ -107,7 +108,7 @@ def extract_plexivity_xml(xml=None):
if a.getElementsByTagName('Player'):
player_elem = a.getElementsByTagName('Player')
for d in player_elem:
ip_address = helpers.get_xml_attr(d, 'address')
ip_address = helpers.get_xml_attr(d, 'address').split('::ffff:')[-1]
machine_id = helpers.get_xml_attr(d, 'machineIdentifier')
platform = helpers.get_xml_attr(d, 'platform')
player = helpers.get_xml_attr(d, 'title')

View File

@@ -44,7 +44,11 @@ def refresh_users():
if user_tokens and user_tokens['server_token']:
pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token'])
library_details = pms_connect.get_server_children()
shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list'])
if library_details:
shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list'])
else:
shared_libraries = ''
control_value_dict = {"user_id": item['user_id']}
new_value_dict = {"username": item['username'],
@@ -108,7 +112,8 @@ def get_real_pms_url():
if connections:
# Get connection with matching address, otherwise return first connection
conn = next((c for c in connections if c['address'] == plexpy.CONFIG.PMS_IP), connections[0])
conn = next((c for c in connections if c['address'] == plexpy.CONFIG.PMS_IP
and c['port'] == str(plexpy.CONFIG.PMS_PORT)), connections[0])
plexpy.CONFIG.__setattr__('PMS_URL', conn['uri'])
plexpy.CONFIG.write()
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
@@ -142,13 +147,15 @@ class PlexTV(object):
if session.get_session_user_id():
user_data = users.Users()
user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
token = user_tokens['server_token']
self.token = user_tokens['server_token']
else:
token = plexpy.CONFIG.PMS_TOKEN
self.token = plexpy.CONFIG.PMS_TOKEN
else:
self.token = token
self.request_handler = http_handler.HTTPHandler(host='plex.tv',
port=443,
token=token,
token=self.token,
ssl_verify=self.ssl_verify)
def get_plex_auth(self, output_format='raw'):

View File

@@ -14,7 +14,7 @@
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
import threading
import urllib2
import urllib
from urlparse import urlparse
import plexpy
@@ -121,13 +121,15 @@ class PmsConnect(object):
if session.get_session_user_id():
user_data = users.Users()
user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
token = user_tokens['server_token']
self.token = user_tokens['server_token']
else:
token = plexpy.CONFIG.PMS_TOKEN
self.token = plexpy.CONFIG.PMS_TOKEN
else:
self.token = token
self.request_handler = http_handler.HTTPHandler(host=hostname,
port=port,
token=token)
token=self.token)
def get_sessions(self, output_format=''):
"""
@@ -179,7 +181,7 @@ class PmsConnect(object):
return request
def get_recently_added(self, count='0', output_format=''):
def get_recently_added(self, start='0', count='0', output_format=''):
"""
Return list of recently added items.
@@ -188,7 +190,7 @@ class PmsConnect(object):
Output: array
"""
uri = '/library/recentlyAdded?X-Plex-Container-Start=0&X-Plex-Container-Size=' + count
uri = '/library/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (start, count)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
@@ -196,7 +198,7 @@ class PmsConnect(object):
return request
def get_library_recently_added(self, section_id='', count='0', output_format=''):
def get_library_recently_added(self, section_id='', start='0', count='0', output_format=''):
"""
Return list of recently added items.
@@ -205,7 +207,7 @@ class PmsConnect(object):
Output: array
"""
uri = '/library/sections/' + section_id + '/recentlyAdded?X-Plex-Container-Start=0&X-Plex-Container-Size=' + count
uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (section_id, start, count)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
@@ -387,7 +389,7 @@ class PmsConnect(object):
Output: array
"""
uri = '/search?query=' + urllib2.quote(query.encode('utf8')) + track
uri = '/search?query=' + urllib.quote(query.encode('utf8')) + track
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
@@ -458,7 +460,7 @@ class PmsConnect(object):
return request
def get_recently_added_details(self, section_id='', count='0'):
def get_recently_added_details(self, section_id='', start='0', count='0'):
"""
Return processed and validated list of recently added items.
@@ -467,9 +469,9 @@ class PmsConnect(object):
Output: array
"""
if section_id:
recent = self.get_library_recently_added(section_id, count, output_format='xml')
recent = self.get_library_recently_added(section_id, start, count, output_format='xml')
else:
recent = self.get_recently_added(count, output_format='xml')
recent = self.get_recently_added(start, count, output_format='xml')
try:
xml_head = recent.getElementsByTagName('MediaContainer')
@@ -1021,6 +1023,8 @@ class PmsConnect(object):
session_output = self.get_session_each(session_type, session_)
session_list.append(session_output)
session_list = sorted(session_list, key=lambda k: k['session_key'])
output = {'stream_count': helpers.get_xml_attr(xml_head[0], 'size'),
'sessions': session.mask_session_info(session_list)
}
@@ -1902,10 +1906,12 @@ class PmsConnect(object):
"""
if img:
uri = '/photo/:/transcode?url=http://127.0.0.1:32400%s' % img
params = {'url': 'http://127.0.0.1:32400%s?%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
if width.isdigit() and height.isdigit():
uri += '&width=%s&height=%s' % (width, height)
params['width'] = width
params['height'] = height
uri = '/photo/:/transcode?%s' % urllib.urlencode(params)
result = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',

View File

@@ -1,2 +1,2 @@
PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.4.0"
PLEXPY_RELEASE_VERSION = "1.4.6"

View File

@@ -86,7 +86,7 @@ def run():
# successfully received data, reset reconnects counter
reconnects = 0
except websocket.WebSocketConnectionClosedException:
except (websocket.WebSocketConnectionClosedException, Exception):
if reconnects <= 15:
reconnects += 1
@@ -94,7 +94,7 @@ def run():
if reconnects > 1:
time.sleep(5)
logger.warn(u"PlexPy WebSocket :: Connection has closed, reconnecting...")
logger.warn(u"PlexPy WebSocket :: Connection has closed, reconnection attempt %s." % reconnects)
try:
ws = create_connection(uri, header=header)
except IOError as e:

View File

@@ -36,7 +36,7 @@ from plexpy.plextv import PlexTV
SESSION_KEY = '_cp_username'
def user_login(username=None, password=None):
if not username and not password:
if not username or not password:
return None
# Try to login to Plex.tv to check if the user has a vaild account
@@ -119,7 +119,7 @@ def check_auth(*args, **kwargs):
if not condition():
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
else:
raise cherrypy.HTTPRedirect("auth/logout")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/logout")
def requireAuth(*conditions):
"""A decorator that appends conditions to the auth.require config
@@ -204,14 +204,14 @@ class AuthController(object):
@cherrypy.expose
def index(self):
raise cherrypy.HTTPRedirect("login")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
@cherrypy.expose
def login(self, username=None, password=None, remember_me='0', admin_login='0'):
if not cherrypy.config.get('tools.sessions.on'):
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
if username is None or password is None:
if not username and not password:
return self.get_loginform()
(vaild_login, user_group) = check_credentials(username, password, admin_login)
@@ -257,4 +257,4 @@ class AuthController(object):
if _session and _session['user']:
cherrypy.request.login = None
self.on_logout(_session['user'], _session['user_group'])
raise cherrypy.HTTPRedirect("login")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")

View File

@@ -34,6 +34,7 @@ import config
import database
import datafactory
import graphs
import helpers
import http_handler
import libraries
import log_reader
@@ -82,9 +83,9 @@ class WebInterface(object):
@requireAuth()
def index(self):
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
raise cherrypy.HTTPRedirect("home")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
else:
raise cherrypy.HTTPRedirect("welcome")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "welcome")
##### Welcome #####
@@ -118,7 +119,7 @@ class WebInterface(object):
# The setup wizard just refreshes the page on submit so we must redirect to home if config set.
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
plexpy.initialize_scheduler()
raise cherrypy.HTTPRedirect("home")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
else:
return serve_template(templatename="welcome.html", title="Welcome", config=config)
@@ -477,9 +478,9 @@ class WebInterface(object):
"get_file_sizes_hold": plexpy.CONFIG.GET_FILE_SIZES_HOLD
}
library_data = libraries.Libraries()
if section_id:
try:
library_data = libraries.Libraries()
library_details = library_data.get_details(section_id=section_id)
except:
logger.warn(u"Unable to retrieve library details for section_id %s " % section_id)
@@ -493,8 +494,8 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def edit_library_dialog(self, section_id=None, **kwargs):
library_data = libraries.Libraries()
if section_id:
library_data = libraries.Libraries()
result = library_data.get_details(section_id=section_id)
status_message = ''
else:
@@ -528,9 +529,9 @@ class WebInterface(object):
do_notify_created = kwargs.get('do_notify_created', 0)
keep_history = kwargs.get('keep_history', 0)
library_data = libraries.Libraries()
if section_id:
try:
library_data = libraries.Libraries()
library_data.set_config(section_id=section_id,
custom_thumb=custom_thumb,
do_notify=do_notify,
@@ -543,7 +544,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def get_library_watch_time_stats(self, section_id=None, **kwargs):
def library_watch_time_stats(self, section_id=None, **kwargs):
if not allow_session_library(section_id):
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@@ -556,12 +557,12 @@ class WebInterface(object):
if result:
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
else:
logger.warn(u"Unable to retrieve data for get_library_watch_time_stats.")
logger.warn(u"Unable to retrieve data for library_watch_time_stats.")
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose
@requireAuth()
def get_library_user_stats(self, section_id=None, **kwargs):
def library_user_stats(self, section_id=None, **kwargs):
if not allow_session_library(section_id):
return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats")
@@ -574,12 +575,12 @@ class WebInterface(object):
if result:
return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats")
else:
logger.warn(u"Unable to retrieve data for get_library_user_stats.")
logger.warn(u"Unable to retrieve data for library_user_stats.")
return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats")
@cherrypy.expose
@requireAuth()
def get_library_recently_watched(self, section_id=None, limit='10', **kwargs):
def library_recently_watched(self, section_id=None, limit='10', **kwargs):
if not allow_session_library(section_id):
return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched")
@@ -592,12 +593,12 @@ class WebInterface(object):
if result:
return serve_template(templatename="user_recently_watched.html", data=result, title="Recently Watched")
else:
logger.warn(u"Unable to retrieve data for get_library_recently_watched.")
logger.warn(u"Unable to retrieve data for library_recently_watched.")
return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched")
@cherrypy.expose
@requireAuth()
def get_library_recently_added(self, section_id=None, limit='10', **kwargs):
def library_recently_added(self, section_id=None, limit='10', **kwargs):
if not allow_session_library(section_id):
return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added")
@@ -610,7 +611,7 @@ class WebInterface(object):
if result:
return serve_template(templatename="library_recently_added.html", data=result['recently_added'], title="Recently Added")
else:
logger.warn(u"Unable to retrieve data for get_library_recently_added.")
logger.warn(u"Unable to retrieve data for library_recently_added.")
return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added")
@cherrypy.expose
@@ -733,6 +734,132 @@ class WebInterface(object):
return {'success': result}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_library(self, section_id=None, **kwargs):
""" Get a library's details.
```
Required parameters:
section_id (str): The id of the Plex library section
Optional parameters:
None
Returns:
json:
{"child_count": null,
"count": 887,
"do_notify": 1,
"do_notify_created": 1,
"keep_history": 1,
"library_art": "/:/resources/movie-fanart.jpg",
"library_thumb": "/:/resources/movie.png",
"parent_count": null,
"section_id": 1,
"section_name": "Movies",
"section_type": "movie"
}
```
"""
if section_id:
library_data = libraries.Libraries()
library_details = library_data.get_details(section_id=section_id)
if library_details:
return library_details
else:
logger.warn(u"Unable to retrieve data for get_library.")
else:
logger.warn(u"Library details requested but no section_id received.")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_library_watch_time_stats(self, section_id=None, **kwargs):
""" Get a library's watch time statistics.
```
Required parameters:
section_id (str): The id of the Plex library section
Optional parameters:
None
Returns:
json:
[{"query_days": 1,
"total_plays": 0,
"total_time": 0
},
{"query_days": 7,
"total_plays": 3,
"total_time": 15694
},
{"query_days": 30,
"total_plays": 35,
"total_time": 63054
},
{"query_days": 0,
"total_plays": 508,
"total_time": 1183080
}
]
```
"""
if section_id:
library_data = libraries.Libraries()
result = library_data.get_watch_time_stats(section_id=section_id)
if result:
return result
else:
logger.warn(u"Unable to retrieve data for get_library_watch_time_stats.")
else:
logger.warn(u"Library watch time stats requested but no section_id received.")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_library_user_stats(self, section_id=None, **kwargs):
""" Get a library's user statistics.
```
Required parameters:
section_id (str): The id of the Plex library section
Optional parameters:
None
Returns:
json:
[{"friendly_name": "Jon Snow",
"total_plays": 170,
"user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar"
},
{"platform_type": "DanyKhaleesi69",
"total_plays": 42,
"user_id": 8008135,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar"
},
{...},
{...}
]
```
"""
if section_id:
library_data = libraries.Libraries()
result = library_data.get_user_stats(section_id=section_id)
if result:
return result
else:
logger.warn(u"Unable to retrieve data for get_library_user_stats.")
else:
logger.warn(u"Library user stats requested but no section_id received.")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@@ -977,9 +1104,9 @@ class WebInterface(object):
if not allow_session_user(user_id):
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
user_data = users.Users()
if user_id:
try:
user_data = users.Users()
user_details = user_data.get_details(user_id=user_id)
except:
logger.warn(u"Unable to retrieve user details for user_id %s " % user_id)
@@ -993,8 +1120,8 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def edit_user_dialog(self, user=None, user_id=None, **kwargs):
user_data = users.Users()
if user_id:
user_data = users.Users()
result = user_data.get_details(user_id=user_id)
status_message = ''
else:
@@ -1030,9 +1157,9 @@ class WebInterface(object):
keep_history = kwargs.get('keep_history', 0)
allow_guest = kwargs.get('allow_guest', 0)
user_data = users.Users()
if user_id:
try:
user_data = users.Users()
user_data.set_config(user_id=user_id,
friendly_name=friendly_name,
custom_thumb=custom_thumb,
@@ -1047,7 +1174,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs):
def user_watch_time_stats(self, user=None, user_id=None, **kwargs):
if not allow_session_user(user_id):
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@@ -1060,12 +1187,12 @@ class WebInterface(object):
if result:
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
else:
logger.warn(u"Unable to retrieve data for get_user_watch_time_stats.")
logger.warn(u"Unable to retrieve data for user_watch_time_stats.")
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose
@requireAuth()
def get_user_player_stats(self, user=None, user_id=None, **kwargs):
def user_player_stats(self, user=None, user_id=None, **kwargs):
if not allow_session_user(user_id):
return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats")
@@ -1078,7 +1205,7 @@ class WebInterface(object):
if result:
return serve_template(templatename="user_player_stats.html", data=result, title="Player Stats")
else:
logger.warn(u"Unable to retrieve data for get_user_player_stats.")
logger.warn(u"Unable to retrieve data for user_player_stats.")
return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats")
@cherrypy.expose
@@ -1170,7 +1297,7 @@ class WebInterface(object):
@requireAuth()
@addtoapi()
def get_user_logins(self, user_id=None, **kwargs):
""" Get the data on PlexPy user login table.
""" Get the data on PlexPy user login table.
```
Required parameters:
@@ -1189,15 +1316,15 @@ class WebInterface(object):
"recordsTotal": 2344,
"recordsFiltered": 10,
"data":
[{"browser": "Safari 7.0.3",
"friendly_name": "Jon Snow",
"host": "http://plexpy.castleblack.com",
"ip_address": "xxx.xxx.xxx.xxx",
"os": "Mac OS X",
"timestamp": 1462591869,
"user": "LordCommanderSnow",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
"user_group": "guest",
[{"browser": "Safari 7.0.3",
"friendly_name": "Jon Snow",
"host": "http://plexpy.castleblack.com",
"ip_address": "xxx.xxx.xxx.xxx",
"os": "Mac OS X",
"timestamp": 1462591869,
"user": "LordCommanderSnow",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
"user_group": "guest",
"user_id": 133788
},
{...},
@@ -1222,6 +1349,134 @@ class WebInterface(object):
return history
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_user(self, user_id=None, **kwargs):
""" Get a user's details.
```
Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
Returns:
json:
{"allow_guest": 1,
"deleted_user": 0,
"do_notify": 1,
"email": "Jon.Snow.1337@CastleBlack.com",
"friendly_name": "Jon Snow",
"is_allow_sync": 1,
"is_home_user": 1,
"is_restricted": 0,
"keep_history": 1,
"shared_libraries": ["10", "1", "4", "5", "15", "20", "2"],
"user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
"username": "LordCommanderSnow"
}
```
"""
if user_id:
user_data = users.Users()
user_details = user_data.get_details(user_id=user_id)
if user_details:
return user_details
else:
logger.warn(u"Unable to retrieve data for get_user.")
else:
logger.warn(u"User details requested but no user_id received.")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_user_watch_time_stats(self, user_id=None, **kwargs):
""" Get a user's watch time statistics.
```
Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
Returns:
json:
[{"query_days": 1,
"total_plays": 0,
"total_time": 0
},
{"query_days": 7,
"total_plays": 3,
"total_time": 15694
},
{"query_days": 30,
"total_plays": 35,
"total_time": 63054
},
{"query_days": 0,
"total_plays": 508,
"total_time": 1183080
}
]
```
"""
if user_id:
user_data = users.Users()
result = user_data.get_watch_time_stats(user_id=user_id)
if result:
return result
else:
logger.warn(u"Unable to retrieve data for get_user_watch_time_stats.")
else:
logger.warn(u"User watch time stats requested but no user_id received.")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_user_player_stats(self, user_id=None, **kwargs):
""" Get a user's player statistics.
```
Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
Returns:
json:
[{"platform_type": "Chrome",
"player_name": "Plex Web (Chrome)",
"result_id": 1,
"total_plays": 170
},
{"platform_type": "Chromecast",
"player_name": "Chromecast",
"result_id": 2,
"total_plays": 42
},
{...},
{...}
]
```
"""
if user_id:
user_data = users.Users()
result = user_data.get_player_stats(user_id=user_id)
if result:
return result
else:
logger.warn(u"Unable to retrieve data for get_user_player_stats.")
else:
logger.warn(u"User watch time stats requested but no user_id received.")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@@ -1240,9 +1495,8 @@ class WebInterface(object):
None
```
"""
user_data = users.Users()
if user_id:
user_data = users.Users()
delete_row = user_data.delete_all_history(user_id=user_id)
if delete_row:
return {'message': delete_row}
@@ -1267,11 +1521,9 @@ class WebInterface(object):
None
```
"""
user_data = users.Users()
if user_id:
user_data = users.Users()
delete_row = user_data.delete(user_id=user_id)
if delete_row:
return {'message': delete_row}
else:
@@ -1296,16 +1548,14 @@ class WebInterface(object):
None
```
"""
user_data = users.Users()
if user_id:
user_data = users.Users()
delete_row = user_data.undelete(user_id=user_id)
if delete_row:
return {'message': delete_row}
elif username:
user_data = users.Users()
delete_row = user_data.undelete(username=username)
if delete_row:
return {'message': delete_row}
else:
@@ -1984,55 +2234,54 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def getLog(self, start=0, length=100, **kwargs):
start = int(start)
length = int(length)
search_value = ""
search_regex = ""
order_column = 0
order_dir = "desc"
def getLog(self, **kwargs):
json_data = helpers.process_json_kwargs(json_kwargs=kwargs.get('json_data'))
log_level = kwargs.get('log_level', "")
if 'order[0][dir]' in kwargs:
order_dir = kwargs.get('order[0][dir]', "desc")
if 'order[0][column]' in kwargs:
order_column = kwargs.get('order[0][column]', "0")
if 'search[value]' in kwargs:
search_value = kwargs.get('search[value]', "")
if 'search[regex]' in kwargs:
search_regex = kwargs.get('search[regex]', "")
start = json_data['start']
length = json_data['length']
order_column = json_data['order'][0]['column']
order_dir = json_data['order'][0]['dir']
search_value = json_data['search']['value']
sortcolumn = 0
filt = []
filtered = []
fa = filt.append
with open(os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)) as f:
for l in f.readlines():
try:
temp_loglevel_and_time = l.split(' - ', 1)
loglvl = temp_loglevel_and_time[1].split(' ::', 1)[0].strip()
msg = l.split(' : ', 1)[1].replace('\n', '')
msg = unicode(l.split(' : ', 1)[1].replace('\n', ''), 'utf-8')
fa([temp_loglevel_and_time[0], loglvl, msg])
except IndexError:
# Add traceback message to previous msg.
tl = (len(filt) - 1)
n = len(l) - len(l.lstrip(' '))
l = '&nbsp;' * (2*n) + l[n:]
l = '&nbsp;' * (2 * n) + l[n:]
filt[tl][2] += '<br>' + l
continue
filtered = []
if search_value == '':
filtered = filt
log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR']
if log_level in log_levels:
log_levels = log_levels[log_levels.index(log_level)::]
filtered = [row for row in filt if row[1] in log_levels]
else:
filtered = [row for row in filt for column in row if search_value.lower() in column.lower()]
filtered = filt
if search_value:
filtered = [row for row in filtered for column in row if search_value.lower() in column.lower()]
sortcolumn = 0
if order_column == '1':
sortcolumn = 2
elif order_column == '2':
sortcolumn = 1
filtered.sort(key=lambda x: x[sortcolumn], reverse=order_dir == "desc")
filtered.sort(key=lambda x: x[sortcolumn])
if order_dir == 'desc':
filtered = filtered[::-1]
rows = filtered[start:(start + length)]
@@ -2046,7 +2295,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_plex_log(self, window=1000, **kwargs):
def get_plex_log(self, **kwargs):
""" Get the PMS logs.
```
@@ -2068,6 +2317,7 @@ class WebInterface(object):
]
```
"""
window = int(kwargs.get('window', plexpy.CONFIG.PMS_LOGS_LINE_CAP))
log_lines = []
log_type = kwargs.get('log_type', 'server')
@@ -2215,7 +2465,7 @@ class WebInterface(object):
log_dir=plexpy.CONFIG.LOG_DIR, verbose=plexpy.VERBOSE)
logger.info(u"Verbose toggled, set to %s", plexpy.VERBOSE)
logger.debug(u"If you read this message, debug logging is available")
raise cherrypy.HTTPRedirect("logs")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "logs")
@cherrypy.expose
@requireAuth()
@@ -2255,6 +2505,7 @@ class WebInterface(object):
config = {
"allow_guest_access": checked(plexpy.CONFIG.ALLOW_GUEST_ACCESS),
"http_basic_auth": checked(plexpy.CONFIG.HTTP_BASIC_AUTH),
"http_hash_password": checked(plexpy.CONFIG.HTTP_HASH_PASSWORD),
"http_hashed_password": plexpy.CONFIG.HTTP_HASHED_PASSWORD,
"http_host": plexpy.CONFIG.HTTP_HOST,
@@ -2262,6 +2513,7 @@ class WebInterface(object):
"http_port": plexpy.CONFIG.HTTP_PORT,
"http_password": http_password,
"http_root": plexpy.CONFIG.HTTP_ROOT,
"http_proxy": checked(plexpy.CONFIG.HTTP_PROXY),
"launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER),
"enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS),
"https_create_cert": checked(plexpy.CONFIG.HTTPS_CREATE_CERT),
@@ -2374,7 +2626,7 @@ class WebInterface(object):
"ip_logging_enable", "movie_logging_enable", "tv_logging_enable", "music_logging_enable",
"notify_consecutive", "notify_upload_posters", "notify_recently_added", "notify_recently_added_grandparent",
"monitor_pms_updates", "monitor_remote_access", "get_file_sizes", "log_blacklist", "http_hash_password",
"allow_guest_access", "cache_images"
"allow_guest_access", "cache_images", "http_proxy", "http_basic_auth"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
@@ -2576,6 +2828,24 @@ class WebInterface(object):
```
Required parameters:
agent_id(str): The id of the notification agent to use
9 # Boxcar2
17 # Browser
10 # Email
16 # Facebook
0 # Growl
12 # IFTTT
18 # Join
4 # NotifyMyAndroid
3 # Plex Home Theater
1 # Prowl
5 # Pushalot
6 # Pushbullet
7 # Pushover
15 # Scripts
14 # Slack
13 # Telegram
11 # Twitter
2 # XBMC
subject(str): The subject of the message
body(str): The body of the message
@@ -2857,7 +3127,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def checkGithub(self):
versioncheck.checkGithub()
raise cherrypy.HTTPRedirect("home")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -2938,13 +3208,17 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth()
def pms_image_proxy(self, img='', rating_key=None, width='0', height='0', fallback=None, **kwargs):
def pms_image_proxy(self, img='', rating_key=None, width='0', height='0',
fallback=None, refresh=False, **kwargs):
""" Gets an image from the PMS and saves it to the image cache directory. """
if not img and not rating_key:
logger.error('No image input received.')
return
refresh = True if refresh == 'true' else False
if rating_key and not img:
img = '/library/metadata/%s/thumb/1337' % rating_key
@@ -2959,8 +3233,9 @@ class WebInterface(object):
os.mkdir(c_dir)
try:
if 'indexes' in img:
if not plexpy.CONFIG.CACHE_IMAGES or refresh or 'indexes' in img:
raise NotFound
return serve_file(path=ffp, content_type='image/jpeg')
except NotFound:
@@ -2980,7 +3255,7 @@ class WebInterface(object):
raise Exception(u'PMS image request failed')
except Exception as e:
logger.exception(u'Failed to get image %s, falling back to %s.' % (img, fallback))
logger.warn(u'Failed to get image %s, falling back to %s.' % (img, fallback))
fbi = None
if fallback == 'poster':
fbi = common.DEFAULT_POSTER_THUMB
@@ -3007,6 +3282,30 @@ class WebInterface(object):
return serve_download(os.path.join(plexpy.CONFIG.LOG_DIR, log_file), name=log_file)
@cherrypy.expose
@requireAuth(member_of("admin"))
@addtoapi()
def download_plex_log(self, **kwargs):
""" Download the Plex log file. """
log_type = kwargs.get('log_type', 'server')
log_file = ""
if plexpy.CONFIG.PMS_LOGS_FOLDER:
if log_type == "server":
log_file = 'Plex Media Server.log'
log_file_path = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, log_file)
elif log_type == "scanner":
log_file = 'Plex Media Scanner.log'
log_file_path = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, log_file)
else:
return "Plex log folder not set in the settings."
if log_file and os.path.isfile(log_file_path):
return serve_download(log_file_path, name=log_file)
else:
return "Plex %s log file not found." % log_type
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@@ -3336,7 +3635,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi("get_recently_added")
def get_recently_added_details(self, count='0', section_id='', **kwargs):
def get_recently_added_details(self, start='0', count='0', section_id='', **kwargs):
""" Get all items that where recelty added to plex.
```
@@ -3344,6 +3643,7 @@ class WebInterface(object):
count (str): Number of items to return
Optional parameters:
start (str): The item number to start at
section_id (str): The id of the Plex library section
Returns:
@@ -3373,7 +3673,7 @@ class WebInterface(object):
```
"""
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_recently_added_details(count=count, section_id=section_id)
result = pms_connect.get_recently_added_details(start=start, count=count, section_id=section_id)
if result:
return result
@@ -3615,19 +3915,22 @@ class WebInterface(object):
}
```
"""
pms_connect = pmsconnect.PmsConnect(token=plexpy.CONFIG.PMS_TOKEN)
result = pms_connect.get_current_activity()
try:
pms_connect = pmsconnect.PmsConnect(token=plexpy.CONFIG.PMS_TOKEN)
result = pms_connect.get_current_activity()
data_factory = datafactory.DataFactory()
for session in result['sessions']:
if not session['ip_address']:
ip_address = data_factory.get_session_ip(session['session_key'])
session['ip_address'] = ip_address
if result:
data_factory = datafactory.DataFactory()
for session in result['sessions']:
if not session['ip_address']:
ip_address = data_factory.get_session_ip(session['session_key'])
session['ip_address'] = ip_address
if result:
return result
else:
logger.warn(u"Unable to retrieve data for get_activity.")
return result
else:
logger.warn(u"Unable to retrieve data for get_activity.")
except Exception as e:
logger.exception(u"Unable to retrieve data for get_activity: %s" % e)
@cherrypy.expose
@cherrypy.tools.json_out()

View File

@@ -66,10 +66,15 @@ def initialize(options):
if options['http_password']:
logger.info(u"PlexPy WebStart :: Web server authentication is enabled, username is '%s'", options['http_username'])
options_dict['tools.sessions.on'] = auth_enabled = session_enabled = True
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
if options['http_basic_auth']:
auth_enabled = session_enabled = False
basic_auth_enabled = True
else:
options_dict['tools.sessions.on'] = auth_enabled = session_enabled = True
basic_auth_enabled = False
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
else:
auth_enabled = session_enabled = False
auth_enabled = session_enabled = basic_auth_enabled = False
if not options['http_root'] or options['http_root'] == '/':
plexpy.HTTP_ROOT = options['http_root'] = '/'
@@ -88,7 +93,14 @@ def initialize(options):
'application/javascript'],
'tools.auth.on': auth_enabled,
'tools.sessions.on': session_enabled,
'tools.sessions.timeout': 30 * 24 * 60 # 30 days
'tools.sessions.timeout': 30 * 24 * 60, # 30 days
'tools.auth_basic.on': basic_auth_enabled,
'tools.auth_basic.realm': 'PlexPy web server',
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
options['http_username']: options['http_password']})
},
'/api': {
'tools.auth_basic.on': False
},
'/interfaces': {
'tools.staticdir.on': True,
@@ -178,17 +190,17 @@ def initialize(options):
'tools.auth.on': False,
'tools.sessions.on': False
},
'/pms_image_proxy': {
'tools.staticdir.on': True,
'tools.staticdir.dir': os.path.join(plexpy.CONFIG.CACHE_DIR, 'images'),
'tools.caching.on': True,
'tools.caching.force': True,
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
},
#'/pms_image_proxy': {
# 'tools.staticdir.on': True,
# 'tools.staticdir.dir': os.path.join(plexpy.CONFIG.CACHE_DIR, 'images'),
# 'tools.caching.on': True,
# 'tools.caching.force': True,
# 'tools.caching.delay': 0,
# 'tools.expires.on': True,
# 'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
# 'tools.auth.on': False,
# 'tools.sessions.on': False
#},
'/favicon.ico': {
'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.abspath(os.path.join(plexpy.PROG_DIR, 'data/interfaces/default/images/favicon.ico')),
@@ -199,7 +211,7 @@ def initialize(options):
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
},
}
}
# Prevent time-outs