Compare commits
121 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6542997520 | ||
![]() |
a58b2e2038 | ||
![]() |
6860e348dc | ||
![]() |
5e094e7597 | ||
![]() |
2f2cb8386b | ||
![]() |
965fd170bd | ||
![]() |
2610d29b60 | ||
![]() |
064131c842 | ||
![]() |
00b6bf8394 | ||
![]() |
2a885d709d | ||
![]() |
5bed46c0aa | ||
![]() |
8b27c7e01a | ||
![]() |
177902a286 | ||
![]() |
48b0f7dc27 | ||
![]() |
d5f4a1a48a | ||
![]() |
de9f60aa7f | ||
![]() |
c93b65b299 | ||
![]() |
3c6a6cdc5b | ||
![]() |
b669f3d715 | ||
![]() |
f663fac220 | ||
![]() |
bc42e79bb5 | ||
![]() |
ca29333cd0 | ||
![]() |
f9f478e100 | ||
![]() |
97c414d1ad | ||
![]() |
7afbd98d17 | ||
![]() |
1f5c60588e | ||
![]() |
284ab45a17 | ||
![]() |
eab6365af9 | ||
![]() |
de86516a0a | ||
![]() |
3e50e11933 | ||
![]() |
e2ac8be451 | ||
![]() |
0e53252a27 | ||
![]() |
b1ecff3d10 | ||
![]() |
0fee4fee2a | ||
![]() |
66282d817c | ||
![]() |
932c93e573 | ||
![]() |
71d30af582 | ||
![]() |
1c8428c3ea | ||
![]() |
e38e98d9e7 | ||
![]() |
85b3f081bf | ||
![]() |
3926d97fc6 | ||
![]() |
13ac8f2ea4 | ||
![]() |
d94f991ab5 | ||
![]() |
d476d2e96a | ||
![]() |
635bf364ac | ||
![]() |
e1c7a37f62 | ||
![]() |
9d780701f5 | ||
![]() |
0bd40405b5 | ||
![]() |
25c2f95e48 | ||
![]() |
5d738e58eb | ||
![]() |
70325f9247 | ||
![]() |
38c9c5a6ea | ||
![]() |
c90dd147bb | ||
![]() |
322f106e75 | ||
![]() |
91a5529438 | ||
![]() |
8f7dd2df6a | ||
![]() |
2fcd55eb60 | ||
![]() |
9359567a8a | ||
![]() |
42bfacfb19 | ||
![]() |
71131c699e | ||
![]() |
6ebfc516a6 | ||
![]() |
5c952b1d86 | ||
![]() |
1d9a4e0b99 | ||
![]() |
ebae628d8d | ||
![]() |
9865460fe5 | ||
![]() |
39884b71fe | ||
![]() |
82b7128c04 | ||
![]() |
16756ddb8c | ||
![]() |
877002961f | ||
![]() |
7e9e68ecd8 | ||
![]() |
6419190272 | ||
![]() |
ac42563c5e | ||
![]() |
98c1063e07 | ||
![]() |
a4dfc57cbe | ||
![]() |
db543b8912 | ||
![]() |
49fb4540a2 | ||
![]() |
e2120393a2 | ||
![]() |
0b301fff3f | ||
![]() |
eeb351e991 | ||
![]() |
1095e29b4d | ||
![]() |
be058eaff7 | ||
![]() |
f409dda2ef | ||
![]() |
f409cdda8f | ||
![]() |
9cd6396c35 | ||
![]() |
ee754ea533 | ||
![]() |
36de20dd75 | ||
![]() |
a957e8eb4f | ||
![]() |
14a90d84ec | ||
![]() |
fae9bc618a | ||
![]() |
3248e6500e | ||
![]() |
c17bf79d79 | ||
![]() |
1ff1270bfa | ||
![]() |
b1a2cf33d8 | ||
![]() |
b2292e98c1 | ||
![]() |
4d156a8911 | ||
![]() |
7193b6518b | ||
![]() |
cff6b44109 | ||
![]() |
fb7ad9438e | ||
![]() |
afc265a188 | ||
![]() |
01fe7bf612 | ||
![]() |
1cb75bd053 | ||
![]() |
0eaea4d011 | ||
![]() |
67377a2561 | ||
![]() |
a8aae9f1f5 | ||
![]() |
a9ce92decb | ||
![]() |
c19162295a | ||
![]() |
58796c45ed | ||
![]() |
d94b348780 | ||
![]() |
95f92bd292 | ||
![]() |
bc52ac3559 | ||
![]() |
8bbc6a6611 | ||
![]() |
8902b93a26 | ||
![]() |
ae36af807d | ||
![]() |
fd256625c6 | ||
![]() |
bee543a25a | ||
![]() |
55eb79cb52 | ||
![]() |
35965a8320 | ||
![]() |
8a902ae3e6 | ||
![]() |
52bed5bf98 | ||
![]() |
9e83f6d779 | ||
![]() |
0873beaed2 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -23,6 +23,9 @@ cache/*
|
|||||||
*.csr
|
*.csr
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
# Mergetool
|
||||||
|
*.orgin
|
||||||
|
|
||||||
# OS generated files #
|
# OS generated files #
|
||||||
######################
|
######################
|
||||||
.DS_Store?
|
.DS_Store?
|
||||||
@@ -32,7 +35,7 @@ Icon?
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
#Ignore files generated by PyCharm
|
#Ignore files generated by PyCharm
|
||||||
.idea/*
|
*.idea/*
|
||||||
|
|
||||||
#Ignore files generated by vi
|
#Ignore files generated by vi
|
||||||
*.swp
|
*.swp
|
||||||
|
98
CHANGELOG.md
98
CHANGELOG.md
@@ -1,5 +1,99 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.3.9 (2016-02-21)
|
||||||
|
|
||||||
|
* Fix: Recently added notification not sent to all notification agents.
|
||||||
|
* Add: Pushover HTML support. (Thanks @elseym)
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.8 (2016-02-21)
|
||||||
|
|
||||||
|
* Fix: Regression unable to clear HTTP password.
|
||||||
|
* Fix: Remove media tags from script arguments for server notifications.
|
||||||
|
* Fix: Encode poster titles to UTF-8 for Imgur upload.
|
||||||
|
* Fix: Allow notifications to send without poster if Imgur upload fails.
|
||||||
|
* Add: Notification Logs table in the Logs tab.
|
||||||
|
* Add: Toggle in settings to enable posters in notifications. (Disabled by default.)
|
||||||
|
* Change: Save Imgur poster URL to database so upload is not needed every time.
|
||||||
|
* Change: Notify log in database to log each event as a separate entry.
|
||||||
|
* Change: Monitor remote access is unchecked if remote access is disabled on server.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.7 (2016-02-20)
|
||||||
|
|
||||||
|
* Fix: Verifying server with SSL enabled.
|
||||||
|
* Fix: Regression where {stream_duration} reported as 0.
|
||||||
|
* Fix: Video metadata flags showing up for track info.
|
||||||
|
* Fix: Custom library icons not applied to Library Statistics.
|
||||||
|
* Fix: Typos in the Web UI.
|
||||||
|
* Add: ETA to Current Activity overlay.
|
||||||
|
* Add: Total duration to Libraries and Users tables.
|
||||||
|
* Add: {machine_id} to notification options.
|
||||||
|
* Add: IMDB, TVDB, TMDb, Last.fm, and Trackt IDs/URLs to notification options.
|
||||||
|
* Add: {poster_url} to notification options using Imgur.
|
||||||
|
* Add: Poster and link for Facebook notifications.
|
||||||
|
* Add: Log javascript errors from the Web UI.
|
||||||
|
* Add: Configuration and Scheduler info to the settings page.
|
||||||
|
* Add: Schedule background task to backup the PlexPy database.
|
||||||
|
* Add: URL anonymizer for external links.
|
||||||
|
* Add: Plex Media Scanner log file to Log viewer.
|
||||||
|
* Add: API v2 (sill very experimental). (Thanks @Hellowlol)
|
||||||
|
* Change: Allow secure websocket connections.
|
||||||
|
* Change: History grouping now accounts for the view offset.
|
||||||
|
* Change: Subject line can be toggled off for Facebook, Slack, Telegram, and Twitter.
|
||||||
|
* Change: Create self-signed SSL certificates when enabling HTTPS.
|
||||||
|
* Change: Revert homepage "Last Played" to "Last Watched".
|
||||||
|
* Change: Disable monitor remote access checkbox if remote access is not enabled on the PMS.
|
||||||
|
* Change: Disable IP logging checkbox if PMS version is 0.9.14 or greater.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.6 (2016-02-03)
|
||||||
|
|
||||||
|
* Fix: Regression where {duration} not reported in minutes.
|
||||||
|
* Fix: Proper daemonizing in FreeBSD and FreeNAS init scripts.
|
||||||
|
* Change: Update readme documentation.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.5 (2016-02-02)
|
||||||
|
|
||||||
|
* Fix: Removing unique constraints from database.
|
||||||
|
* Fix: Unable to expand media info table when missing "Added At" date.
|
||||||
|
* Fix: Server verification for unpublished servers.
|
||||||
|
* Fix: Updating PMS identifier for server change.
|
||||||
|
* Add: {stream_time}, {remaining_time}, and {progress_time} to notification options.
|
||||||
|
* Add: Powershell script support. (Thanks @Hellowlol)
|
||||||
|
* Add: Method to delete duplicate libraries.
|
||||||
|
* Change: Daemonize before running start up tasks.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.4 (2016-01-29)
|
||||||
|
|
||||||
|
* Fix: Activity checker not starting with library update (history not logging).
|
||||||
|
* Fix: Libraries duplicated in database.
|
||||||
|
* Fix: Buffer notifications even when disabled when using websockets.
|
||||||
|
* Fix: Libraries and Users lists not refreshing.
|
||||||
|
* Fix: Server verification in settings.
|
||||||
|
* Fix: Empty libraries not added to database.
|
||||||
|
* Add: Unique identifiers to notification options.
|
||||||
|
* Remove: Requirement of media type toggles for recently added notifications.
|
||||||
|
* Remove: Built in Twitter key and secret.
|
||||||
|
* Change: Unnecessary quoting of script arguments.
|
||||||
|
* Change: Facebook notification instructions.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.3 (2016-01-26)
|
||||||
|
|
||||||
|
* Fix: Plays by Month graph not loading.
|
||||||
|
* Change: Disable caching for datatables.
|
||||||
|
* Change: Improved updating library data in the database again.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.3.2 (2016-01-24)
|
||||||
|
|
||||||
|
* Fix: 'datestamp' and 'timestamp' for server notifications.
|
||||||
|
* Change: New method for updating library data in database.
|
||||||
|
|
||||||
|
|
||||||
## v1.3.1 (2016-01-23)
|
## v1.3.1 (2016-01-23)
|
||||||
|
|
||||||
* Fix: Notifiers authorization popups for reverse proxies.
|
* Fix: Notifiers authorization popups for reverse proxies.
|
||||||
@@ -7,8 +101,8 @@
|
|||||||
* Fix: Star rating overlapping text.
|
* Fix: Star rating overlapping text.
|
||||||
* Fix: Unable to startup when library refresh fails.
|
* Fix: Unable to startup when library refresh fails.
|
||||||
* Fix: Unable to parse 'datestamp' and 'timestamp' format.
|
* Fix: Unable to parse 'datestamp' and 'timestamp' format.
|
||||||
* Change: Rename "Last Watched" to "Last Played"
|
* Change: Rename "Last Watched" to "Last Played".
|
||||||
* Change: More descriptive libraries updating message
|
* Change: More descriptive libraries updating message.
|
||||||
|
|
||||||
|
|
||||||
## v1.3.0 (2016-01-23)
|
## v1.3.0 (2016-01-23)
|
||||||
|
@@ -3,10 +3,43 @@
|
|||||||
## Issues
|
## Issues
|
||||||
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs, improvements or feature requests. However, we are glad to help you, and make sure the problem is not caused by PlexPy, but don't expect step-by-step answers.
|
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs, improvements or feature requests. However, we are glad to help you, and make sure the problem is not caused by PlexPy, but don't expect step-by-step answers.
|
||||||
|
|
||||||
* Use the search function. Chances are that your problem is already discussed. Do not append to (closed) issues if your problem does not fit the discussion.
|
##### Many issues can simply be solved by:
|
||||||
* Visit the [Troubleshooting](../../wiki/TroubleShooting) wiki first.
|
|
||||||
* Use [proper formatting](https://help.github.com/articles/github-flavored-markdown/). Paste your logs in code blocks.
|
- Making sure you update to the latest version.
|
||||||
* Close your issue if you resolved it.
|
- Turning your device off and on again.
|
||||||
|
- Analyzing your logs, you just might find the solution yourself!
|
||||||
|
- Using the **search** function to see if this issue has already been reported/solved.
|
||||||
|
- Checking the [Wiki](https://github.com/drzoidberg33/plexpy/wiki) for
|
||||||
|
[ [Installation] ](https://github.com/drzoidberg33/plexpy/wiki/Installation) and
|
||||||
|
[ [FAQs] ](https://github.com/drzoidberg33/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
|
||||||
|
- For basic questions try asking on [Gitter](https://gitter.im/drzoidberg33/plexpy) or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
|
||||||
|
|
||||||
|
##### If nothing has worked:
|
||||||
|
|
||||||
|
1. Open a new issue on the GitHub [issue tracker](http://github.com/drzoidberg33/plexpy/issues).
|
||||||
|
2. Provide a clear title to easily help identify your problem.
|
||||||
|
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
|
||||||
|
4. Make sure you provide the following information:
|
||||||
|
- [ ] Version
|
||||||
|
- [ ] Branch
|
||||||
|
- [ ] Commit hash
|
||||||
|
- [ ] Operating system
|
||||||
|
- [ ] Python version
|
||||||
|
- [ ] What you did?
|
||||||
|
- [ ] What happened?
|
||||||
|
- [ ] What you expected?
|
||||||
|
- [ ] How can we reproduce your issue?
|
||||||
|
- [ ] What are your (relevant) settings?
|
||||||
|
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
|
||||||
|
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
|
||||||
|
|
||||||
|
## Feature Requests
|
||||||
|
|
||||||
|
1. Search for similar existing 'issues', feature requests can be recognized by the blue `enhancement` label.
|
||||||
|
2. If a similar request exists, post a comment (+1, or add a new idea to the existing request).
|
||||||
|
3. If no similar requests exist, you can create a new one.
|
||||||
|
4. Provide a clear title to easily identify the feature request.
|
||||||
|
5. Tag your feature request with `[Feature Request]` so it can be identified easily.
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
If you think you can contribute code to the PlexPy repository, do not hesitate to submit a pull request.
|
If you think you can contribute code to the PlexPy repository, do not hesitate to submit a pull request.
|
||||||
|
@@ -153,12 +153,12 @@ def main():
|
|||||||
# Put the database in the DATA_DIR
|
# Put the database in the DATA_DIR
|
||||||
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db')
|
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db')
|
||||||
|
|
||||||
# Read config and start logging
|
|
||||||
plexpy.initialize(config_file)
|
|
||||||
|
|
||||||
if plexpy.DAEMON:
|
if plexpy.DAEMON:
|
||||||
plexpy.daemonize()
|
plexpy.daemonize()
|
||||||
|
|
||||||
|
# Read config and start logging
|
||||||
|
plexpy.initialize(config_file)
|
||||||
|
|
||||||
# Force the http port if neccessary
|
# Force the http port if neccessary
|
||||||
if args.port:
|
if args.port:
|
||||||
http_port = args.port
|
http_port = args.port
|
||||||
|
168
README.md
168
README.md
@@ -1,121 +1,77 @@
|
|||||||
#PlexPy
|
# PlexPy
|
||||||
|
|
||||||
[](https://gitter.im/drzoidberg33/plexpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/drzoidberg33/plexpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
A python based web application for monitoring, analytics and notifications for Plex Media Server (www.plex.tv).
|
A python based web application for monitoring, analytics and notifications for Plex Media Server (www.plex.tv).
|
||||||
|
|
||||||
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).
|
This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).
|
||||||
|
|
||||||
* PlexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
|
* PlexPy [forum thread](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* Responsive web design viewable on desktop, tablet and mobile web browsers.
|
||||||
|
* Themed to complement Plex/Web.
|
||||||
|
* Easy configuration setup (no separate web server required).
|
||||||
|
* Monitor current Plex Media Server activity.
|
||||||
|
* Fully customizable notifications for stream activity and recently added media.
|
||||||
|
* Top statistics on home page with configurable duration and measurement metric.
|
||||||
|
* Global watching history with search/filtering & dynamic column sorting.
|
||||||
|
* Full user list with general information and comparison stats.
|
||||||
|
* Individual user information including devices IP addresses.
|
||||||
|
* Complete library statistics and media file information.
|
||||||
|
* Rich analytics presented using Highcharts graphing.
|
||||||
|
* Beautiful content information pages.
|
||||||
|
* Full sync list data on all users syncing items from your library.
|
||||||
|
* And many more!!
|
||||||
|
|
||||||
###Support
|
## Installation and Support
|
||||||
-----------
|
|
||||||
* PlexPy Wiki: https://github.com/drzoidberg33/plexpy/wiki
|
|
||||||
|
|
||||||
|
* [Installation Guides](https://github.com/drzoidberg33/plexpy/wiki/Installation) shows you how to install PlexPy.
|
||||||
###Features
|
* [FAQs](https://github.com/drzoidberg33/plexpy/wiki/Frequently-Asked-Questions-(FAQ)) in the wiki can help you with common problems.
|
||||||
-----------
|
|
||||||
* Responsive web design viewable on desktop, tablet and mobile web browsers
|
|
||||||
|
|
||||||
* Themed to complement Plex/Web
|
|
||||||
|
|
||||||
* Easy configuration setup via html form
|
|
||||||
|
|
||||||
* Current Plex Media Server viewing activity including:
|
|
||||||
* number of current users
|
|
||||||
* title
|
|
||||||
* progress
|
|
||||||
* platform
|
|
||||||
* user
|
|
||||||
* state (playing, paused, buffering, etc)
|
|
||||||
* stream type (direct, transcoded)
|
|
||||||
* video type & resolution
|
|
||||||
* audio type & channel count.
|
|
||||||
|
|
||||||
* Top statistics on home page with configurable duration and measurement metric:
|
|
||||||
* Most watched TV
|
|
||||||
* Most popular TV
|
|
||||||
* Most watched Movie
|
|
||||||
* Most popular Movie
|
|
||||||
* Most active user
|
|
||||||
* Most active platform
|
|
||||||
|
|
||||||
* Recently added media and how long ago it was added
|
|
||||||
|
|
||||||
* Global watching history with search/filtering & dynamic column sorting
|
|
||||||
* date
|
|
||||||
* user
|
|
||||||
* platform
|
|
||||||
* ip address
|
|
||||||
* title
|
|
||||||
* stream information details
|
|
||||||
* start time
|
|
||||||
* paused duration length
|
|
||||||
* stop time
|
|
||||||
* duration length
|
|
||||||
* watched progress
|
|
||||||
* show/hide columns
|
|
||||||
* delete mode - allows deletion of specific history items
|
|
||||||
|
|
||||||
* Full user list with general information and comparison stats
|
|
||||||
|
|
||||||
* Individual user information
|
|
||||||
* username and gravatar (if available)
|
|
||||||
* daily, weekly, monthly, all time stats for play count and duration length
|
|
||||||
* individual platform stats for each user
|
|
||||||
* public ip address history with last seen date and geo tag location
|
|
||||||
* recently watched content
|
|
||||||
* watching history
|
|
||||||
* synced items
|
|
||||||
* assign users custom friendly names within PlexPy
|
|
||||||
* assign users custom avatar URL within PlexPy
|
|
||||||
* disable history logging per user
|
|
||||||
* disable notifications per user
|
|
||||||
* option to purge all history per user.
|
|
||||||
|
|
||||||
* Rich analytics presented using Highcharts graphing
|
|
||||||
* user-selectable time periods of 30, 90 or 365 days
|
|
||||||
* daily watch count and duration
|
|
||||||
* totals by day of week and hours of the day
|
|
||||||
* totals by top 10 platform
|
|
||||||
* totals by top 10 users
|
|
||||||
* detailed breakdown by transcode decision
|
|
||||||
* source and stream resolutions
|
|
||||||
* transcode decision counts by user and platform
|
|
||||||
* total monthly counts
|
|
||||||
|
|
||||||
* Content information pages
|
|
||||||
* movies (includes watching history)
|
|
||||||
* tv shows (includes watching history)
|
|
||||||
* tv seasons
|
|
||||||
* tv episodes (includes watching history)
|
|
||||||
|
|
||||||
* Full sync list data on all users syncing items from your library
|
|
||||||
|
|
||||||
## Installation and Notes
|
|
||||||
|
|
||||||
* [Installation page](../../wiki/Installation) shows you how to install PlexPy.
|
|
||||||
* [Usage guide](../../wiki/Usage-guide) introduces you to PlexPy.
|
|
||||||
* [Troubleshooting page](../../wiki/TroubleShooting) in the wiki can help you with common problems.
|
|
||||||
|
|
||||||
**Issues** can be reported on the GitHub issue tracker considering these rules:
|
|
||||||
|
|
||||||
1. Analyze your log, you just might find the solution yourself!
|
|
||||||
2. You read the wiki and searched existing issues, but this is not solving your problem.
|
|
||||||
3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
|
|
||||||
4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
|
|
||||||
|
|
||||||
**Feature requests** can be reported on the GitHub issue tracker too:
|
|
||||||
|
|
||||||
1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'.
|
|
||||||
2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one.
|
|
||||||
|
|
||||||
If you **comply with these rules** you can [post your request/issue](http://github.com/drzoidberg33/plexpy/issues).
|
|
||||||
|
|
||||||
**Support** the project by implementing new features, solving support tickets and provide bug fixes.
|
**Support** the project by implementing new features, solving support tickets and provide bug fixes.
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
##### Many issues can simply be solved by:
|
||||||
|
|
||||||
|
- Making sure you update to the latest version.
|
||||||
|
- Turning your device off and on again.
|
||||||
|
- Analyzing your logs, you just might find the solution yourself!
|
||||||
|
- Using the **search** function to see if this issue has already been reported/solved.
|
||||||
|
- Checking the [Wiki](https://github.com/drzoidberg33/plexpy/wiki) for
|
||||||
|
[ [Installation] ](https://github.com/drzoidberg33/plexpy/wiki/Installation) and
|
||||||
|
[ [FAQs] ](https://github.com/drzoidberg33/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
|
||||||
|
- For basic questions try asking on [Gitter](https://gitter.im/drzoidberg33/plexpy) or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
|
||||||
|
|
||||||
|
##### If nothing has worked:
|
||||||
|
|
||||||
|
1. Open a new issue on the GitHub [issue tracker](http://github.com/drzoidberg33/plexpy/issues).
|
||||||
|
2. Provide a clear title to easily help identify your problem.
|
||||||
|
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
|
||||||
|
4. Make sure you provide the following information:
|
||||||
|
- [ ] Version
|
||||||
|
- [ ] Branch
|
||||||
|
- [ ] Commit hash
|
||||||
|
- [ ] Operating system
|
||||||
|
- [ ] Python version
|
||||||
|
- [ ] What you did?
|
||||||
|
- [ ] What happened?
|
||||||
|
- [ ] What you expected?
|
||||||
|
- [ ] How can we reproduce your issue?
|
||||||
|
- [ ] What are your (relevant) settings?
|
||||||
|
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
|
||||||
|
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
|
||||||
|
|
||||||
|
## Feature Requests
|
||||||
|
|
||||||
|
1. Search for similar existing 'issues', feature requests can be recognized by the blue `enhancement` label.
|
||||||
|
2. If a similar request exists, post a comment (+1, or add a new idea to the existing request).
|
||||||
|
3. If no similar requests exist, you can create a new one.
|
||||||
|
4. Provide a clear title to easily identify the feature request.
|
||||||
|
5. Tag your feature request with `[Feature Request]` so it can be identified easily.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
|
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
|
||||||
|
|
||||||
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
|
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
|
@@ -501,7 +501,8 @@ textarea.form-control:focus {
|
|||||||
.libraries-poster-face {
|
.libraries-poster-face {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
float: left;
|
float: left;
|
||||||
background-size: contain;
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
@@ -1717,7 +1718,8 @@ a:hover .item-children-poster {
|
|||||||
float: left;
|
float: left;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
background-size: contain;
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
@@ -2178,6 +2180,10 @@ a .home-platforms-instance-list-oval:hover,
|
|||||||
.refresh-libraries-button {
|
.refresh-libraries-button {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
.refresh-users-button,
|
||||||
|
.refresh-libraries-button {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
.nav-settings,
|
.nav-settings,
|
||||||
.nav-settings ul {
|
.nav-settings ul {
|
||||||
margin: 0px 0px 20px 0px;
|
margin: 0px 0px 20px 0px;
|
||||||
@@ -2712,4 +2718,44 @@ table[id^='media_info_child'] table[id^='media_info_child'] thead th {
|
|||||||
}
|
}
|
||||||
.selectize-input input[type='text'] {
|
.selectize-input input[type='text'] {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
}
|
||||||
|
.small-muted {
|
||||||
|
font-size: small;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.config-info-table,
|
||||||
|
.config-scheduler-table {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
.config-info-table td,
|
||||||
|
.config-info-table th,
|
||||||
|
.config-scheduler-table td,
|
||||||
|
.config-scheduler-table th {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.config-info-table td:first-child {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
.config-scheduler-table td:first-child {
|
||||||
|
width: 225px;
|
||||||
|
}
|
||||||
|
.config-scheduler-table th {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
a.no-highlight {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
a.no-highlight:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.top-line {
|
||||||
|
border-top: 1px dotted #777;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
.help-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.save-button {
|
||||||
|
margin-top: 15px;
|
||||||
}
|
}
|
@@ -198,6 +198,13 @@ DOCUMENTATION :: END
|
|||||||
% else:
|
% else:
|
||||||
<span>IP: N/A</span>
|
<span>IP: N/A</span>
|
||||||
% endif
|
% endif
|
||||||
|
<br />
|
||||||
|
ETA:
|
||||||
|
<span id="stream-eta-${a['session_key']}">
|
||||||
|
<script>
|
||||||
|
$("#stream-eta-${a['session_key']}").html(moment().add(parseInt(${a['duration']}) - parseInt(${a['view_offset']}), 'milliseconds').format(time_format));
|
||||||
|
</script>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-poster-info-time">
|
<div class="dashboard-activity-poster-info-time">
|
||||||
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
|
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
|
||||||
|
@@ -692,7 +692,7 @@ DOCUMENTATION :: END
|
|||||||
<li>
|
<li>
|
||||||
<div class="home-platforms-instance-info">
|
<div class="home-platforms-instance-info">
|
||||||
<div class="home-platforms-instance-name">
|
<div class="home-platforms-instance-name">
|
||||||
<h4>Last Played</h4>
|
<h4>Last Watched</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="home-platforms-instance-last-user">
|
<div class="home-platforms-instance-last-user">
|
||||||
<h4>
|
<h4>
|
||||||
|
@@ -171,10 +171,10 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
% if data['media_type'] == 'movie' or data['media_type'] == 'episode' or data['media_type'] == 'track':
|
% if data['media_type'] == 'movie' or data['media_type'] == 'episode' or data['media_type'] == 'track':
|
||||||
<div class="summary-content-media-info-wrapper">
|
<div class="summary-content-media-info-wrapper">
|
||||||
% if data['video_codec']:
|
% if data['media_type'] != 'track' and data['video_codec']:
|
||||||
<img class="summary-content-media-flag" title="${data['video_codec']}" src="interfaces/default/images/media_flags/video_codec/${data['video_codec'] | vf}.png" />
|
<img class="summary-content-media-flag" title="${data['video_codec']}" src="interfaces/default/images/media_flags/video_codec/${data['video_codec'] | vf}.png" />
|
||||||
% endif
|
% endif
|
||||||
% if data['video_resolution']:
|
% if data['media_type'] != 'track' and data['video_resolution']:
|
||||||
<img class="summary-content-media-flag" title="${data['video_resolution']}" src="interfaces/default/images/media_flags/video_resolution/${data['video_resolution']}.png" />
|
<img class="summary-content-media-flag" title="${data['video_resolution']}" src="interfaces/default/images/media_flags/video_resolution/${data['video_resolution']}.png" />
|
||||||
% endif
|
% endif
|
||||||
% if data['audio_codec']:
|
% if data['audio_codec']:
|
||||||
|
@@ -54,7 +54,7 @@ function showMsg(msg,loader,timeout,ms,error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doAjaxCall(url,elem,reload,form) {
|
function doAjaxCall(url, elem, reload, form, callback) {
|
||||||
// Set Message
|
// Set Message
|
||||||
feedback = $("#ajaxMsg");
|
feedback = $("#ajaxMsg");
|
||||||
update = $("#updatebar");
|
update = $("#updatebar");
|
||||||
@@ -157,6 +157,9 @@ function doAjaxCall(url,elem,reload,form) {
|
|||||||
complete: function(jqXHR, textStatus) {
|
complete: function(jqXHR, textStatus) {
|
||||||
// Remove loaders and stuff, ajax request is complete!
|
// Remove loaders and stuff, ajax request is complete!
|
||||||
loader.remove();
|
loader.remove();
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -252,13 +255,13 @@ function isPrivateIP(ip_address) {
|
|||||||
|
|
||||||
function humanTime(seconds) {
|
function humanTime(seconds) {
|
||||||
if (seconds >= 86400) {
|
if (seconds >= 86400) {
|
||||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) +
|
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' +
|
||||||
'</h3><p> days </p><h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) +
|
'<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' +
|
||||||
'</h3><p> hrs</p><h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
'<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||||
return text;
|
return text;
|
||||||
} else if (seconds >= 3600) {
|
} else if (seconds >= 3600) {
|
||||||
text = '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) +
|
text = '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' +
|
||||||
'</h3><p>hrs</p><h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
'<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||||
return text;
|
return text;
|
||||||
} else if (seconds >= 60) {
|
} else if (seconds >= 60) {
|
||||||
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||||
@@ -269,6 +272,25 @@ function humanTime(seconds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function humanTimeClean(seconds) {
|
||||||
|
if (seconds >= 86400) {
|
||||||
|
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' +
|
||||||
|
Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' +
|
||||||
|
Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||||
|
return text;
|
||||||
|
} else if (seconds >= 3600) {
|
||||||
|
text = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' +
|
||||||
|
Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||||
|
return text;
|
||||||
|
} else if (seconds >= 60) {
|
||||||
|
text = Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
text = '0';
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String.prototype.toProperCase = function () {
|
String.prototype.toProperCase = function () {
|
||||||
return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
|
return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
|
||||||
};
|
};
|
||||||
@@ -372,3 +394,16 @@ function clearSearchButton(tableName, table) {
|
|||||||
table.search('').draw();
|
table.search('').draw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Taken from https://github.com/Hellowlol/HTPC-Manager
|
||||||
|
window.onerror = function (message, file, line) {
|
||||||
|
var e = {
|
||||||
|
'page': window.location.href,
|
||||||
|
'message': message,
|
||||||
|
'file': file,
|
||||||
|
'line': line
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post("log_js_errors", e, function (data) {
|
||||||
|
});
|
||||||
|
};
|
@@ -22,7 +22,7 @@ history_table_options = {
|
|||||||
"emptyTable": "No data in table"
|
"emptyTable": "No data in table"
|
||||||
},
|
},
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"processing": false,
|
"processing": false,
|
||||||
"serverSide": true,
|
"serverSide": true,
|
||||||
"pageLength": 25,
|
"pageLength": 25,
|
||||||
|
@@ -16,7 +16,7 @@ libraries_list_table_options = {
|
|||||||
"pageLength": 10,
|
"pageLength": 10,
|
||||||
"order": [ 2, 'asc'],
|
"order": [ 2, 'asc'],
|
||||||
"autoWidth": true,
|
"autoWidth": true,
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"columnDefs": [
|
"columnDefs": [
|
||||||
{
|
{
|
||||||
@@ -161,12 +161,28 @@ libraries_list_table_options = {
|
|||||||
$(td).html('n/a');
|
$(td).html('n/a');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "25%",
|
"width": "18%",
|
||||||
"className": "hidden-sm hidden-xs"
|
"className": "hidden-sm hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [9],
|
"targets": [9],
|
||||||
"data": "plays",
|
"data": "plays",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== null && cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchable": false,
|
||||||
|
"width": "7%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [10],
|
||||||
|
"data": "duration",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== null && cellData !== '') {
|
||||||
|
$(td).html(humanTimeClean(cellData));
|
||||||
|
}
|
||||||
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"width": "10%"
|
"width": "10%"
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ var log_table_options = {
|
|||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"order": [ 0, 'desc'],
|
"order": [ 0, 'desc'],
|
||||||
"pageLength": 50,
|
"pageLength": 50,
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"language": {
|
"language": {
|
||||||
"search":"Search: ",
|
"search":"Search: ",
|
||||||
"lengthMenu":"Show _MENU_ lines per page",
|
"lengthMenu":"Show _MENU_ lines per page",
|
||||||
|
@@ -34,9 +34,12 @@ media_info_table_options = {
|
|||||||
"targets": [0],
|
"targets": [0],
|
||||||
"data": "added_at",
|
"data": "added_at",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== null && cellData !== '') {
|
if (rowData) {
|
||||||
var expand_details = '';
|
var expand_details = '';
|
||||||
var date = moment(cellData, "X").format(date_format);
|
var date = '';
|
||||||
|
if (cellData !== null && cellData !== '') {
|
||||||
|
date = moment(cellData, "X").format(date_format);
|
||||||
|
}
|
||||||
if (rowData['media_type'] === 'show') {
|
if (rowData['media_type'] === 'show') {
|
||||||
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Seasons"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Seasons"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
||||||
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
||||||
|
89
data/interfaces/default/js/tables/notification_logs.js
Normal file
89
data/interfaces/default/js/tables/notification_logs.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
notification_log_table_options = {
|
||||||
|
"destroy": true,
|
||||||
|
"serverSide": true,
|
||||||
|
"processing": false,
|
||||||
|
"pagingType": "bootstrap",
|
||||||
|
"order": [ 0, 'desc'],
|
||||||
|
"pageLength": 50,
|
||||||
|
"stateSave": false,
|
||||||
|
"language": {
|
||||||
|
"search":"Search: ",
|
||||||
|
"lengthMenu":"Show _MENU_ lines per page",
|
||||||
|
"emptyTable": "No log information available",
|
||||||
|
"info":"Showing _START_ to _END_ of _TOTAL_ lines",
|
||||||
|
"infoEmpty":"Showing 0 to 0 of 0 lines",
|
||||||
|
"infoFiltered":"(filtered from _MAX_ total lines)"},
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"targets": [0],
|
||||||
|
"data": "timestamp",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(moment(cellData, "X").format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%",
|
||||||
|
"className": "no-wrap hidden-xs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [1],
|
||||||
|
"data": "agent_name",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "7%",
|
||||||
|
"className": "no-wrap hidden-sm hidden-xs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [2],
|
||||||
|
"data": "notify_action",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "5%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [3],
|
||||||
|
"data": "subject_text",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "20%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [4],
|
||||||
|
"data": "body_text",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "38%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [5],
|
||||||
|
"data": "script_args",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "20%"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"drawCallback": function (settings) {
|
||||||
|
// Jump to top of page
|
||||||
|
//$('html,body').scrollTop(0);
|
||||||
|
$('#ajaxMsg').fadeOut();
|
||||||
|
},
|
||||||
|
"preDrawCallback": function(settings) {
|
||||||
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
|
showMsg(msg, false, false, 0)
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@ var plex_log_table_options = {
|
|||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"order": [ 0, 'desc'],
|
"order": [ 0, 'desc'],
|
||||||
"pageLength": 50,
|
"pageLength": 50,
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"language": {
|
"language": {
|
||||||
"search":"Search: ",
|
"search":"Search: ",
|
||||||
"lengthMenu":"Show _MENU_ lines per page",
|
"lengthMenu":"Show _MENU_ lines per page",
|
||||||
|
@@ -4,7 +4,7 @@ sync_table_options = {
|
|||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"order": [ [ 0, 'desc'], [ 1, 'asc'], [2, 'asc'] ],
|
"order": [ [ 0, 'desc'], [ 1, 'asc'], [2, 'asc'] ],
|
||||||
"pageLength": 25,
|
"pageLength": 25,
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"language": {
|
"language": {
|
||||||
"search":"Search: ",
|
"search":"Search: ",
|
||||||
"lengthMenu":"Show _MENU_ lines per page",
|
"lengthMenu":"Show _MENU_ lines per page",
|
||||||
|
@@ -8,7 +8,7 @@ user_ip_table_options = {
|
|||||||
"infoFiltered":"(filtered from _MAX_ total entries)",
|
"infoFiltered":"(filtered from _MAX_ total entries)",
|
||||||
"emptyTable": "No data in table",
|
"emptyTable": "No data in table",
|
||||||
},
|
},
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"processing": false,
|
"processing": false,
|
||||||
"serverSide": true,
|
"serverSide": true,
|
||||||
|
@@ -16,7 +16,7 @@ users_list_table_options = {
|
|||||||
"pageLength": 10,
|
"pageLength": 10,
|
||||||
"order": [ 2, 'asc'],
|
"order": [ 2, 'asc'],
|
||||||
"autoWidth": true,
|
"autoWidth": true,
|
||||||
"stateSave": true,
|
"stateSave": false,
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"columnDefs": [
|
"columnDefs": [
|
||||||
{
|
{
|
||||||
@@ -165,12 +165,28 @@ users_list_table_options = {
|
|||||||
$(td).html('n/a');
|
$(td).html('n/a');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "30%",
|
"width": "23%",
|
||||||
"className": "hidden-sm hidden-xs"
|
"className": "hidden-sm hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [8],
|
"targets": [8],
|
||||||
"data": "plays",
|
"data": "plays",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== null && cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchable": false,
|
||||||
|
"width": "7%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [9],
|
||||||
|
"data": "duration",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== null && cellData !== '') {
|
||||||
|
$(td).html(humanTimeClean(cellData));
|
||||||
|
}
|
||||||
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"width": "10%"
|
"width": "10%"
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
|
||||||
|
<link rel="stylesheet" href="interfaces/default/css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
<div id="update_section_ids_message" style="text-align: center; margin-top: 20px;">
|
<div id="update_section_ids_message" style="text-align: center; margin-top: 20px;">
|
||||||
<i class="fa fa-refresh fa-spin"></i> PlexPy is updating library IDs in the database. This could take a few minutes to hours depending on the size of your database.
|
<i class="fa fa-refresh fa-spin"></i> PlexPy is updating library IDs in the database. This could take a few minutes to hours depending on the size of your database.
|
||||||
<br />
|
<br />
|
||||||
You may leave this page and come back later. Note: All monitoring has been disabled while this update is in progress.
|
You may leave this page and come back later.
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
<span><i class="fa fa-book"></i> All Libraries</span>
|
<span><i class="fa fa-book"></i> All Libraries</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
|
<div class="colvis-button-bar hidden-xs"></div>
|
||||||
% if config['update_section_ids'] == -1:
|
% if config['update_section_ids'] == -1:
|
||||||
<button class="btn btn-dark refresh-libraries-button" id="refresh-libraries-list" disabled><i class="fa fa-refresh"></i> Refresh libraries</button>
|
<button class="btn btn-dark refresh-libraries-button" id="refresh-libraries-list" disabled><i class="fa fa-refresh"></i> Refresh libraries</button>
|
||||||
% else:
|
% else:
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
<th align="left" id="last_accessed">Last Accessed</th>
|
<th align="left" id="last_accessed">Last Accessed</th>
|
||||||
<th align="left" id="last_played">Last Played</th>
|
<th align="left" id="last_played">Last Played</th>
|
||||||
<th align="left" id="total_plays">Total Plays</th>
|
<th align="left" id="total_plays">Total Plays</th>
|
||||||
|
<th align="left" id="total_duration">Total Duration</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="interfaces/default/js/dataTables.colVis.js"></script>
|
||||||
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
||||||
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
@@ -96,6 +100,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options);
|
libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options);
|
||||||
|
var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
|
||||||
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
clearSearchButton('libraries_list_table', libraries_list_table);
|
clearSearchButton('libraries_list_table', libraries_list_table);
|
||||||
|
|
||||||
@@ -177,7 +183,7 @@
|
|||||||
$("#refresh-libraries-list").click(function () {
|
$("#refresh-libraries-list").click(function () {
|
||||||
if ("${config['update_section_ids']}" == "1") {
|
if ("${config['update_section_ids']}" == "1") {
|
||||||
$('#update_section_ids_message').html(
|
$('#update_section_ids_message').html(
|
||||||
'<i class="fa fa-refresh fa-spin"></i> PlexPy is updating library IDs in the database. This could take a few minutes depending on the size of your database.' +
|
'<i class="fa fa-refresh fa-spin"></i> PlexPy is updating library IDs in the database. This could take a few minutes to hours depending on the size of your database.' +
|
||||||
'<br />' +
|
'<br />' +
|
||||||
'You may leave this page and come back later.');
|
'You may leave this page and come back later.');
|
||||||
$(this).prop('disabled', true);
|
$(this).prop('disabled', true);
|
||||||
|
@@ -37,7 +37,9 @@ DOCUMENTATION :: END
|
|||||||
% if data:
|
% if data:
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<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>
|
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['library_art']}&width=1920&height=1080)"></div>
|
||||||
|
% endif
|
||||||
<div class="summary-container">
|
<div class="summary-container">
|
||||||
<div class="summary-navbar">
|
<div class="summary-navbar">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@@ -52,7 +54,7 @@ DOCUMENTATION :: END
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<div class="user-info-wrapper">
|
<div class="user-info-wrapper">
|
||||||
% if data['library_thumb'][:4] == 'http':
|
% if data['library_thumb'][:4] == 'http' or data['library_thumb'][:10] == 'interfaces':
|
||||||
<div class="library-info-poster-face" style="background-image: url(${data['library_thumb']});"></div>
|
<div class="library-info-poster-face" style="background-image: url(${data['library_thumb']});"></div>
|
||||||
% else:
|
% else:
|
||||||
<div class="library-info-poster-face" style="background-image: url(pms_image_proxy?img=${data['library_thumb']}&width=80&height=80&fallback=cover);"></div>
|
<div class="library-info-poster-face" style="background-image: url(pms_image_proxy?img=${data['library_thumb']}&width=80&height=80&fallback=cover);"></div>
|
||||||
|
@@ -91,6 +91,6 @@ DOCUMENTATION :: END
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="text-muted">Unable to retrieve data from database.
|
<div class="text-muted">No stats to show.
|
||||||
</div><br>
|
</div><br>
|
||||||
% endif
|
% endif
|
@@ -75,13 +75,13 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% if library['thumb']:
|
% if library['thumb'].startswith("http"):
|
||||||
<div class="home-platforms-instance-poster">
|
<div class="home-platforms-instance-poster">
|
||||||
<div class="home-platforms-library-thumb" style="background-image: url(pms_image_proxy?img=${library['thumb']}&width=300&height=300&fallback=poster);"></div>
|
<div class="home-platforms-library-thumb" style="background-image: url(${library['thumb']});"></div>
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="home-platforms-instance-poster">
|
<div class="home-platforms-instance-poster">
|
||||||
<div class="home-platforms-library-thumb" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
<div class="home-platforms-library-thumb" style="background-image: url(pms_image_proxy?img=${library['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
</li>
|
</li>
|
||||||
|
@@ -29,6 +29,8 @@ from plexpy import helpers
|
|||||||
<ul class="nav nav-pills" role="tablist">
|
<ul class="nav nav-pills" role="tablist">
|
||||||
<li role="presentation" class="active"><a id="plexpy-logs-btn" href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">PlexPy Logs</a></li>
|
<li role="presentation" class="active"><a id="plexpy-logs-btn" href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">PlexPy Logs</a></li>
|
||||||
<li role="presentation"><a id="plex-logs-btn" href="#tabs-2" aria-controls="tabs-2" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
|
<li role="presentation"><a id="plex-logs-btn" href="#tabs-2" aria-controls="tabs-2" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
|
||||||
|
<li role="presentation"><a id="plex-scanner-logs-btn" href="#tabs-3" aria-controls="tabs-3" role="tab" data-toggle="tab">Plex Media Scanner Logs</a></li>
|
||||||
|
<li role="presentation"><a id="notification-logs-btn" href="#tabs-4" aria-controls="tabs-4" role="tab" data-toggle="tab">Notification Logs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
||||||
@@ -57,6 +59,33 @@ from plexpy import helpers
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-3">
|
||||||
|
<table class="display" id="plex_scanner_log_table" width="100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align='left' id="plex_scanner_timestamp">Timestamp</th>
|
||||||
|
<th align='left' id="plex_scanner_level">Level</th>
|
||||||
|
<th align='left' id="plex_scanner_message">Message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-4">
|
||||||
|
<table class="display" id="notification_log_table" width="100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align='left' id="notification_timestamp">Timestamp</th>
|
||||||
|
<th align='left' id="notification_agent_name">Agent</th>
|
||||||
|
<th align='left' id="notification_action">Action</th>
|
||||||
|
<th align='left' id="notification_poster_url">Subject Text</th>
|
||||||
|
<th align='left' id="notification_poster_url">Body Text</th>
|
||||||
|
<th align='left' id="notification_poster_url">Script Args</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,8 +109,10 @@ from plexpy import helpers
|
|||||||
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
||||||
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
||||||
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
||||||
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
<script src="interfaces/default/js/tables/logs.js"></script>
|
<script src="interfaces/default/js/tables/logs.js"></script>
|
||||||
<script src="interfaces/default/js/tables/plex_logs.js"></script>
|
<script src="interfaces/default/js/tables/plex_logs.js"></script>
|
||||||
|
<script src="interfaces/default/js/tables/notification_logs.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@@ -91,31 +122,63 @@ from plexpy import helpers
|
|||||||
|
|
||||||
function LoadPlexPyLogs() {
|
function LoadPlexPyLogs() {
|
||||||
log_table_options.ajax = {
|
log_table_options.ajax = {
|
||||||
"url": "getLog"
|
url: "getLog"
|
||||||
}
|
}
|
||||||
log_table = $('#log_table').DataTable(log_table_options);
|
log_table = $('#log_table').DataTable(log_table_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadPlexLogs() {
|
function LoadPlexLogs() {
|
||||||
plex_log_table_options.ajax = {
|
plex_log_table_options.ajax = {
|
||||||
"url": "get_plex_log"
|
url: "get_plex_log?log_type=server"
|
||||||
}
|
}
|
||||||
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
|
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#plexpy-logs-btn").click(function() {
|
function LoadPlexScannerLogs() {
|
||||||
|
plex_log_table_options.ajax = {
|
||||||
|
url: "get_plex_log?log_type=scanner"
|
||||||
|
}
|
||||||
|
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadNotificationLogs() {
|
||||||
|
notification_log_table_options.ajax = {
|
||||||
|
url: "get_notification_log",
|
||||||
|
type: 'post',
|
||||||
|
data: function (d) {
|
||||||
|
return {
|
||||||
|
json_data: JSON.stringify(d)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notification_log_table = $('#notification_log_table').DataTable(notification_log_table_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#plexpy-logs-btn").click(function () {
|
||||||
$("#clear-logs").show();
|
$("#clear-logs").show();
|
||||||
LoadPlexPyLogs();
|
LoadPlexPyLogs();
|
||||||
clearSearchButton('log_table', log_table);
|
clearSearchButton('log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#plex-logs-btn").click(function() {
|
$("#plex-logs-btn").click(function () {
|
||||||
$("#clear-logs").hide();
|
$("#clear-logs").hide();
|
||||||
LoadPlexLogs();
|
LoadPlexLogs();
|
||||||
clearSearchButton('plex_log_table', plex_log_table);
|
clearSearchButton('plex_log_table', plex_log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-logs").click(function() {
|
$("#plex-scanner-logs-btn").click(function () {
|
||||||
|
$("#clear-logs").hide();
|
||||||
|
LoadPlexScannerLogs();
|
||||||
|
clearSearchButton('plex_scanner_log_table', plex_scanner_log_table);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#notification-logs-btn").click(function () {
|
||||||
|
$("#clear-logs").hide();
|
||||||
|
LoadNotificationLogs();
|
||||||
|
clearSearchButton('notification_log_table', notification_log_table);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#clear-logs").click(function () {
|
||||||
var r = confirm("Are you sure you want to clear the PlexPy log?");
|
var r = confirm("Are you sure you want to clear the PlexPy log?");
|
||||||
if (r == true) {
|
if (r == true) {
|
||||||
window.location.href = "clearLogs";
|
window.location.href = "clearLogs";
|
||||||
|
@@ -132,7 +132,7 @@ from plexpy import helpers
|
|||||||
function reloadModal() {
|
function reloadModal() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_notification_agent_config',
|
url: 'get_notification_agent_config',
|
||||||
data: { config_id: '${agent["id"]}' },
|
data: { agent_id: '${agent["id"]}' },
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
@@ -147,9 +147,8 @@ from plexpy import helpers
|
|||||||
})
|
})
|
||||||
|
|
||||||
$('#save-notification-item').click(function () {
|
$('#save-notification-item').click(function () {
|
||||||
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
|
||||||
// Reload modal to update certain fields
|
// Reload modal to update certain fields
|
||||||
reloadModal();
|
doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,6 +180,10 @@ from plexpy import helpers
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#facebookStep1').click(function () {
|
$('#facebookStep1').click(function () {
|
||||||
|
// Remove trailing '/' from Facebook redirect URI
|
||||||
|
if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) {
|
||||||
|
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
|
||||||
|
}
|
||||||
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
||||||
$.get('facebookStep1', function (data) { window.open(data); })
|
$.get('facebookStep1', function (data) { window.open(data); })
|
||||||
.done(function () { showMsg('<i class="fa fa-check"></i> Confirm Authorization. Check pop-up blocker if no response.', false, true, 3000); });
|
.done(function () { showMsg('<i class="fa fa-check"></i> Confirm Authorization. Check pop-up blocker if no response.', false, true, 3000); });
|
||||||
@@ -191,7 +194,7 @@ from plexpy import helpers
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'test_notifier',
|
url: 'test_notifier',
|
||||||
data: {
|
data: {
|
||||||
config_id: '${agent["id"]}',
|
agent_id: '${agent["id"]}',
|
||||||
subject: $('#test_subject').val(),
|
subject: $('#test_subject').val(),
|
||||||
body: $('#test_body').val(),
|
body: $('#test_body').val(),
|
||||||
script: $('#test_script').val(),
|
script: $('#test_script').val(),
|
||||||
@@ -207,8 +210,8 @@ from plexpy import helpers
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#pushbullet_apikey, #pushover_apitoken, #scripts_folder').on('change', function () {
|
$('#pushbullet_apikey, #pushover_apitoken, #scripts_folder').on('change', function () {
|
||||||
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
// Reload modal to update certain fields
|
||||||
reloadModal();
|
doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -64,13 +64,6 @@ from plexpy import helpers
|
|||||||
</label>
|
</label>
|
||||||
<p class="help-block">Trigger notification when a media item is added to the Plex Media Server.</p>
|
<p class="help-block">Trigger notification when a media item is added to the Plex Media Server.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extdown" ${helpers.checked(data['on_extdown'])} class="toggle-switches">
|
|
||||||
Notify on Plex remote access down
|
|
||||||
</label>
|
|
||||||
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached externally.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intdown" ${helpers.checked(data['on_intdown'])} class="toggle-switches">
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intdown" ${helpers.checked(data['on_intdown'])} class="toggle-switches">
|
||||||
@@ -78,13 +71,6 @@ from plexpy import helpers
|
|||||||
</label>
|
</label>
|
||||||
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached internally.</p>
|
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached internally.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extup" ${helpers.checked(data['on_extup'])} class="toggle-switches">
|
|
||||||
Notify on Plex remote access back up
|
|
||||||
</label>
|
|
||||||
<p class="help-block">Trigger notification when the Plex Media Server can be reached externally after being down.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intup" ${helpers.checked(data['on_intup'])} class="toggle-switches">
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intup" ${helpers.checked(data['on_intup'])} class="toggle-switches">
|
||||||
@@ -92,6 +78,20 @@ from plexpy import helpers
|
|||||||
</label>
|
</label>
|
||||||
<p class="help-block">Trigger notification when the Plex Media Server can be reached internally after being down.</p>
|
<p class="help-block">Trigger notification when the Plex Media Server can be reached internally after being down.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extdown" ${helpers.checked(data['on_extdown'])} class="toggle-switches">
|
||||||
|
Notify on Plex remote access down
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached externally.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extup" ${helpers.checked(data['on_extup'])} class="toggle-switches">
|
||||||
|
Notify on Plex remote access back up
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when the Plex Media Server can be reached externally after being down.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
64
data/interfaces/default/scheduler_table.html
Normal file
64
data/interfaces/default/scheduler_table.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<%doc>
|
||||||
|
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
||||||
|
|
||||||
|
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
||||||
|
|
||||||
|
Filename: scheduler_table.html
|
||||||
|
Version: 0.1
|
||||||
|
|
||||||
|
DOCUMENTATION :: END
|
||||||
|
</%doc>
|
||||||
|
|
||||||
|
<%!
|
||||||
|
import arrow
|
||||||
|
import plexpy
|
||||||
|
from plexpy import common
|
||||||
|
|
||||||
|
scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
|
||||||
|
%>
|
||||||
|
|
||||||
|
<table class="config-scheduler-table small-muted">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Scheduled Task</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Interval</th>
|
||||||
|
<th>Next Run In</th>
|
||||||
|
<th>Next Run Time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for job in common.SCHEDULER_LIST:
|
||||||
|
% if job in scheduled_jobs:
|
||||||
|
<%
|
||||||
|
sched_job = plexpy.SCHED.get_job(job)
|
||||||
|
run_interval = arrow.get(str(sched_job.trigger.interval), ['H:mm:ss', 'HH:mm:ss'])
|
||||||
|
next_run_interval = arrow.get(sched_job.next_run_time).timestamp - arrow.now().timestamp
|
||||||
|
%>
|
||||||
|
<tr>
|
||||||
|
<td>${sched_job.id}</td>
|
||||||
|
<td><i class="fa fa-sm fa-fw fa-check"></i> Active</td>
|
||||||
|
<td>${arrow.get(run_interval).format('HH:mm:ss')}</td>
|
||||||
|
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
|
||||||
|
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
% elif job == 'Check for active sessions' and plexpy.CONFIG.MONITORING_USE_WEBSOCKET and not plexpy.POLLING_FAILOVER:
|
||||||
|
<tr>
|
||||||
|
<td>${job}</td>
|
||||||
|
<td><i class="fa fa-sm fa-fw fa-check"></i> Using Websocket</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
</tr>
|
||||||
|
% else:
|
||||||
|
<tr>
|
||||||
|
<td>${job}</td>
|
||||||
|
<td><i class="fa fa-sm fa-fw fa-times"></i> Inactive</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
@@ -1,7 +1,9 @@
|
|||||||
<%inherit file="base.html"/>
|
<%inherit file="base.html"/>
|
||||||
<%!
|
<%!
|
||||||
|
import sys
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import notifiers, common, versioncheck
|
from plexpy import notifiers, common, versioncheck
|
||||||
|
from plexpy.helpers import anon_url
|
||||||
|
|
||||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['name'])
|
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['name'])
|
||||||
%>
|
%>
|
||||||
@@ -33,7 +35,8 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<!-- Nav tabs -->
|
<!-- Nav tabs -->
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<ul class="nav-settings list-unstyled" role="tablist">
|
<ul class="nav-settings list-unstyled" role="tablist">
|
||||||
<li role="presentation" class="active"><a href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">General</a></li>
|
<li role="presentation" class="active"><a href="#tabs-0" aria-controls="tabs-0" role="tab" data-toggle="tab">Help & Info</a></li>
|
||||||
|
<li role="presentation"><a href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">General</a></li>
|
||||||
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" role="tab" data-toggle="tab">Homepage Statistics</a></li>
|
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" role="tab" data-toggle="tab">Homepage Statistics</a></li>
|
||||||
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" role="tab" data-toggle="tab">Web Interface</a></li>
|
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" role="tab" data-toggle="tab">Web Interface</a></li>
|
||||||
<li role="presentation"><a href="#tabs-4" aria-controls="tabs-4" role="tab" data-toggle="tab">Access Control</a></li>
|
<li role="presentation"><a href="#tabs-4" aria-controls="tabs-4" role="tab" data-toggle="tab">Access Control</a></li>
|
||||||
@@ -48,21 +51,97 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
|
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
<div role="tabpanel" class="tab-pane active" id="tabs-0">
|
||||||
% if common.VERSION_NUMBER:
|
% if common.VERSION_NUMBER:
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
<h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
|
<h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>PlexPy Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<table class="config-info-table small-muted">
|
||||||
|
<tbody>
|
||||||
|
% if plexpy.CURRENT_VERSION:
|
||||||
|
<tr>
|
||||||
|
<td>Git Branch:</td>
|
||||||
|
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy/tree/%s' % plexpy.CONFIG.GIT_BRANCH)}">${plexpy.CONFIG.GIT_BRANCH}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Git Commit Hash:</td>
|
||||||
|
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy/commit/%s' % plexpy.CURRENT_VERSION)}">${plexpy.CURRENT_VERSION}</a></td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
<tr>
|
||||||
|
<td>Configuration File:</td>
|
||||||
|
<td>${plexpy.CONFIG_FILE}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Database File:</td>
|
||||||
|
<td>${plexpy.DB_FILE}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Backup Directory:</td>
|
||||||
|
<td>${plexpy.CONFIG.BACKUP_DIR}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cache Directory:</td>
|
||||||
|
<td>${plexpy.CONFIG.CACHE_DIR}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Log Directory:</td>
|
||||||
|
<td>${plexpy.CONFIG.LOG_DIR}</td>
|
||||||
|
</tr>
|
||||||
|
% if plexpy.ARGS:
|
||||||
|
<tr>
|
||||||
|
<td>Arguments:</td>
|
||||||
|
<td>${plexpy.ARGS}</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
<tr>
|
||||||
|
<td>Platform:</td>
|
||||||
|
<td>${common.PLATFORM} ${common.PLATFORM_VERSION}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Python Version:</td>
|
||||||
|
<td>${sys.version}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="top-line">Plex Forums:</td>
|
||||||
|
<td class="top-line"><a class="no-highlight" href="${anon_url('https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program')}" target="_blank">https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Wiki:</td>
|
||||||
|
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy/wiki')}" target="_blank">https://github.com/drzoidberg33/plexpy/wiki</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Source:</td>
|
||||||
|
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy')}" target="_blank">https://github.com/drzoidberg33/plexpy</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Gitter Chat:</td>
|
||||||
|
<td><a class="no-highlight" href="${anon_url('https://gitter.im/drzoidberg33/plexpy')}" target="_blank">https://gitter.im/drzoidberg33/plexpy</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>PlexPy Scheduler</h3>
|
||||||
|
</div>
|
||||||
|
<div id="plexpy-scheduler-table">
|
||||||
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading scheduler table...</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-1">
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Updates</h3>
|
||||||
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
|
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
|
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
|
||||||
</div>
|
</div>
|
||||||
% if plexpy.CURRENT_VERSION:
|
|
||||||
<p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
|
|
||||||
% endif
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
<h3>Display Settings</h3>
|
<h3>Display Settings</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +224,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<li class="card card-sortable">
|
<li class="card card-sortable">
|
||||||
<div class="card-handle"><i class="fa fa-bars"></i></div>
|
<div class="card-handle"><i class="fa fa-bars"></i></div>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="hscard-last_watched" name="hscard-last_watched" value="last_watched"> Last Played
|
<input type="checkbox" id="hscard-last_watched" name="hscard-last_watched" value="last_watched"> Last Watched
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li class="card card-sortable">
|
<li class="card card-sortable">
|
||||||
@@ -256,16 +335,51 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<p class="help-block">Enable HTTPS for web server for encrypted communication.</p>
|
<p class="help-block">Enable HTTPS for web server for encrypted communication.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="https_options">
|
<div id="https_options">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="http-settings" name="https_create_cert" id="https_create_cert" value="1" ${config['https_create_cert']} /> Create Self-signed Certificate
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Check to have PlexPy create a self-signed SSL certificate. Uncheck if you want to use your own certificate.</p>
|
||||||
|
</div>
|
||||||
|
<div id="https_options_self-signed">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="https_domain">HTTPS Domains</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="text" class="form-control http-settings" id="https_domain" name="https_domain" value="${config['https_domain']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">The domain names used to access PlexPy, separated by commas (,).</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="https_ip">HTTPS IPs</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="text" class="form-control http-settings" id="https_ip" name="https_ip" value="${config['https_ip']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">The IP addresses used to access PlexPy, separated by commas (,).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="https_cert">HTTPS Cert</label>
|
<label for="https_cert">HTTPS Cert</label>
|
||||||
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">The location of the SSL certificate.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="https_key">HTTPS Key</label>
|
<label for="https_key">HTTPS Key</label>
|
||||||
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">The location of the SSL key.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-4">
|
<div role="tabpanel" class="tab-pane" id="tabs-4">
|
||||||
@@ -355,10 +469,14 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Force SSL
|
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Use SSL
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Force PlexPy to connect to your Plex Server via SSL. Your server needs to have remote access enabled.</p>
|
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||||
|
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
<h3>Plex Logs</h3>
|
<h3>Plex Logs</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,11 +489,9 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Set the complete folder path where your Plex Server logs are, shortcuts are not recognized.<br />
|
<p class="help-block">Set the complete folder path where your Plex Server logs are, shortcuts are not recognized.<br />
|
||||||
<a href="https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files" target="_blank">Click here</a> for help. This is required if you enable IP logging (for PMS 0.9.12 and below). </p>
|
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank">Click here</a> for help. This is required if you enable IP logging (for PMS 0.9.12 and below). </p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
|
||||||
|
|
||||||
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
|
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -455,10 +571,18 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes [experimental]
|
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes <span style="color: #eb8600; padding-left: 10px;">[experimental]</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Enable if you want PlexPy to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.<br />
|
<p class="help-block">Enable if you want PlexPy to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.</p>
|
||||||
This is currently experimental.</p>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="anon_redirect">Anonymous Redirect</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="text" class="form-control" id="anon_redirect" name="anon_redirect" value="${config['anon_redirect']}" size="30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">Backlink protection via anonymizer service, must end in "?".</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
@@ -485,16 +609,16 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" class="monitor-settings" id="monitoring_use_websocket" name="monitoring_use_websocket" value="1" ${config['monitoring_use_websocket']}> Use Websocket (requires restart) [experimental]
|
<input type="checkbox" class="monitor-settings" id="monitoring_use_websocket" name="monitoring_use_websocket" value="1" ${config['monitoring_use_websocket']}> Use Websocket (requires restart) <span style="color: #eb8600; padding-left: 10px;">[experimental]</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Instead of polling the server at regular intervals let the server tell us when something happens.<br />
|
<p class="help-block">Instead of polling the server at regular intervals let the server tell PlexPy when something happens.</p>
|
||||||
This is currently experimental. Encrypted websocket is not currently supported.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
|
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Enable to have PlexPy check if remote access to the Plex Media Server goes down. Your server needs to have remote access enabled.</p>
|
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
||||||
|
<p class="help-block">Enable to have PlexPy check if remote access to the Plex Media Server goes down.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
@@ -532,7 +656,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</label>
|
</label>
|
||||||
<span id="debugLogCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
<span id="debugLogCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Enable this to attempt to log the IP address of the user (for PMS 0.9.12 and below, IP address is automatically logged for PMS 0.9.14 and above).
|
Enable this to attempt to log the IP address of the user.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -589,6 +713,13 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox" style="padding-top: 15px;">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="notify_upload_posters" id="notify_upload_posters" value="1" ${config['notify_upload_posters']}> Enable Posters in Notifications
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Enable to upload Plex posters to Imgur for notifications. Disable if posters are not being used to save bandwidth.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
<h3>Current Activity Notifications</h3>
|
<h3>Current Activity Notifications</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -637,11 +768,11 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</div>
|
</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
You can set custom formatted text for each type of notification.
|
You can set custom formatted text for each type of notification.
|
||||||
Click <a href="#notify-text-sub-modal" data-toggle="modal">here</a> for a list of available parameters which can be used.
|
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a list of available parameters which can be used.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
You can also add tags to exclude certain text depending on the media type. Click
|
You can also add tags to exclude certain text depending on the media type.
|
||||||
<a href="#notify-text-tags-modal" data-toggle="modal">here</a> to view usage information.
|
<a href="#notify-text-tags-modal" data-toggle="modal">Click here</a> to view usage information.
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<ul id="accordion-session" class="accordion list-unstyled">
|
<ul id="accordion-session" class="accordion list-unstyled">
|
||||||
@@ -766,23 +897,6 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Remote Access Down<i class="fa fa-chevron-down"></i></div>
|
|
||||||
<ul class="submenu">
|
|
||||||
<li>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="notify_on_extdown_subject_text">Subject Line</label>
|
|
||||||
<input class="form-control" type="text" id="notify_on_extdown_subject_text" name="notify_on_extdown_subject_text" value="${config['notify_on_extdown_subject_text']}" data-parsley-trigger="change" required>
|
|
||||||
<p class="help-block">Set a custom subject line.</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="notify_on_extdown_body_text">Message Body</label>
|
|
||||||
<textarea class="form-control" id="notify_on_extdown_body_text" name="notify_on_extdown_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extdown_body_text']}</textarea>
|
|
||||||
<p class="help-block">Set a custom body.</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Server Down<i class="fa fa-chevron-down"></i></div>
|
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Server Down<i class="fa fa-chevron-down"></i></div>
|
||||||
<ul class="submenu">
|
<ul class="submenu">
|
||||||
@@ -800,23 +914,6 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Remote Access Back Up<i class="fa fa-chevron-down"></i></div>
|
|
||||||
<ul class="submenu">
|
|
||||||
<li>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="notify_on_extup_subject_text">Subject Line</label>
|
|
||||||
<input class="form-control" type="text" id="notify_on_extup_subject_text" name="notify_on_extup_subject_text" value="${config['notify_on_extup_subject_text']}" data-parsley-trigger="change" required>
|
|
||||||
<p class="help-block">Set a custom subject line.</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="notify_on_extup_body_text">Message Body</label>
|
|
||||||
<textarea class="form-control" id="notify_on_extup_body_text" name="notify_on_extup_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extup_body_text']}</textarea>
|
|
||||||
<p class="help-block">Set a custom body.</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Server Back Up<i class="fa fa-chevron-down"></i></div>
|
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Server Back Up<i class="fa fa-chevron-down"></i></div>
|
||||||
<ul class="submenu">
|
<ul class="submenu">
|
||||||
@@ -834,6 +931,40 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Remote Access Down<i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="submenu">
|
||||||
|
<li>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notify_on_extdown_subject_text">Subject Line</label>
|
||||||
|
<input class="form-control" type="text" id="notify_on_extdown_subject_text" name="notify_on_extdown_subject_text" value="${config['notify_on_extdown_subject_text']}" data-parsley-trigger="change" required>
|
||||||
|
<p class="help-block">Set a custom subject line.</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notify_on_extdown_body_text">Message Body</label>
|
||||||
|
<textarea class="form-control" id="notify_on_extdown_body_text" name="notify_on_extdown_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extdown_body_text']}</textarea>
|
||||||
|
<p class="help-block">Set a custom body.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="link"><i class="fa fa-server fa-fw"></i> Plex Remote Access Back Up<i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="submenu">
|
||||||
|
<li>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notify_on_extup_subject_text">Subject Line</label>
|
||||||
|
<input class="form-control" type="text" id="notify_on_extup_subject_text" name="notify_on_extup_subject_text" value="${config['notify_on_extup_subject_text']}" data-parsley-trigger="change" required>
|
||||||
|
<p class="help-block">Set a custom subject line.</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notify_on_extup_body_text">Message Body</label>
|
||||||
|
<textarea class="form-control" id="notify_on_extup_body_text" name="notify_on_extup_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extup_body_text']}</textarea>
|
||||||
|
<p class="help-block">Set a custom body.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul id="accordion-scripts" class="accordion list-unstyled">
|
<ul id="accordion-scripts" class="accordion list-unstyled">
|
||||||
<li>
|
<li>
|
||||||
@@ -857,7 +988,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<h3>Notification Agents</h3>
|
<h3>Notification Agents</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Toggle the desired notification options by clicking the bell icon and configure it by clicking the settings icon to the right.
|
Toggle the desired notification options by clicking the <span class="help-bold">bell icon (<i class="fa fa-sm fa-bell"></i>)</span> and configure it by clicking the settings icon to the right.
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<ul class="stacked-configs list-unstyled">
|
<ul class="stacked-configs list-unstyled">
|
||||||
@@ -1013,8 +1144,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
class="fa fa-remove"></i></button>
|
|
||||||
<h4 class="modal-title">Fetch Plex.tv Token</h4>
|
<h4 class="modal-title">Fetch Plex.tv Token</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="modal-text">
|
<div class="modal-body" id="modal-text">
|
||||||
@@ -1072,7 +1202,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
Server Details
|
Global
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1085,6 +1215,18 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<td><strong>{server_uptime}</strong></td>
|
<td><strong>{server_uptime}</strong></td>
|
||||||
<td>The uptime (in days, hours, mins, secs) of your Plex Server.</td>
|
<td>The uptime (in days, hours, mins, secs) of your Plex Server.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{action}</strong></td>
|
||||||
|
<td>The action that triggered the notification.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{datestamp}</strong></td>
|
||||||
|
<td>The date (in date format) the notification was triggered.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{timestamp}</strong></td>
|
||||||
|
<td>The time (in time format) the notification was triggered.</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="notification-params">
|
<table class="notification-params">
|
||||||
@@ -1100,18 +1242,6 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<td><strong>{streams}</strong></td>
|
<td><strong>{streams}</strong></td>
|
||||||
<td>The number of concurrent streams.</td>
|
<td>The number of concurrent streams.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><strong>{action}</strong></td>
|
|
||||||
<td>The action that triggered the notification.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>{datestamp}</strong></td>
|
|
||||||
<td>The date the notification was triggered.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>{timestamp}</strong></td>
|
|
||||||
<td>The time the notification was triggered.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{user}</strong></td>
|
<td><strong>{user}</strong></td>
|
||||||
<td>The username of the person streaming.</td>
|
<td>The username of the person streaming.</td>
|
||||||
@@ -1126,24 +1256,32 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{ip_address}</strong></td>
|
<td><strong>{ip_address}</strong></td>
|
||||||
<td>The IP address of the device being used for playback. (PMS 0.9.14 and above)</td>
|
<td>The IP address of the device being used for playback. <span class="small-muted">(PMS 0.9.14 and above)</span></td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>{media_type}</strong></td>
|
|
||||||
<td>The type of media being played (movie, episode, track).</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{stream_duration}</strong></td>
|
<td><strong>{stream_duration}</strong></td>
|
||||||
<td>The stream duration (in minutes) for the item.</td>
|
<td>The stream duration (in minutes) for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{stream_time}</strong></td>
|
||||||
|
<td>The stream duration (in time format) for the item.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{remaining_duration}</strong></td>
|
<td><strong>{remaining_duration}</strong></td>
|
||||||
<td>The remaining duration (in minutes) for the item.</td>
|
<td>The remaining duration (in minutes) for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{progress}</strong></td>
|
<td><strong>{remaining_time}</strong></td>
|
||||||
|
<td>The remaining duration (in time format) for the item.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{progress_duration}</strong></td>
|
||||||
<td>The last reported offset (in minutes) for the item.</td>
|
<td>The last reported offset (in minutes) for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{progress_time}</strong></td>
|
||||||
|
<td>The last reported offset (in time format) for the item.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{progress_percent}</strong></td>
|
<td><strong>{progress_percent}</strong></td>
|
||||||
<td>The last reported progress percent for the item.</td>
|
<td>The last reported progress percent for the item.</td>
|
||||||
@@ -1224,6 +1362,18 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<td><strong>{transcode_audio_channels}</strong></td>
|
<td><strong>{transcode_audio_channels}</strong></td>
|
||||||
<td>The audio channels of the transcoded media.</td>
|
<td>The audio channels of the transcoded media.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{session_key}</strong></td>
|
||||||
|
<td>The unique identifier for the session.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{user_id}</strong></td>
|
||||||
|
<td>The unique identifier for the user.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{machine_id}</strong></td>
|
||||||
|
<td>The unique identifier for the player.</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="notification-params">
|
<table class="notification-params">
|
||||||
@@ -1235,37 +1385,41 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{media_type}</strong></td>
|
||||||
|
<td>The type of media. <span class="small-muted">(movie, episode, track)</span></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{title}</strong></td>
|
<td><strong>{title}</strong></td>
|
||||||
<td>The full title of the item being played.</td>
|
<td>The full title of the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{library_name}</strong></td>
|
<td><strong>{library_name}</strong></td>
|
||||||
<td>The library title of the item being played.</td>
|
<td>The library title of the media item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{show_name}</strong></td>
|
<td><strong>{show_name}</strong></td>
|
||||||
<td>The title of the TV series being played.</td>
|
<td>The title of the TV series.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{episode_name}</strong></td>
|
<td><strong>{episode_name}</strong></td>
|
||||||
<td>The title of the episode being played.</td>
|
<td>The title of the episode.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{artist_name}</strong></td>
|
<td><strong>{artist_name}</strong></td>
|
||||||
<td>The name of the artist being played.</td>
|
<td>The name of the artistd.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{album_name}</strong></td>
|
<td><strong>{album_name}</strong></td>
|
||||||
<td>The title of the album being played.</td>
|
<td>The title of the album.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{track_name}</strong></td>
|
<td><strong>{track_name}</strong></td>
|
||||||
<td>The title of the track being played.</td>
|
<td>The title of the track.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{season_num}</strong></td>
|
<td><strong>{season_num}</strong></td>
|
||||||
<td>The season number for the media item if item is episode.</td>
|
<td>The season number for the episode.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{season_num00}</strong></td>
|
<td><strong>{season_num00}</strong></td>
|
||||||
@@ -1273,43 +1427,51 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{episode_num}</strong></td>
|
<td><strong>{episode_num}</strong></td>
|
||||||
<td>The episode number for the media item if item is episode.</td>
|
<td>The episode number for the episode.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{episode_num00}</strong></td>
|
<td><strong>{episode_num00}</strong></td>
|
||||||
<td>The two digit episode number.</td>
|
<td>The two digit episode number.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{track_num}</strong></td>
|
||||||
|
<td>The track number for the track.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{track_num00}</strong></td>
|
||||||
|
<td>The two digit track number.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{year}</strong></td>
|
<td><strong>{year}</strong></td>
|
||||||
<td>The release year for the media item.</td>
|
<td>The release year for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{studio}</strong></td>
|
<td><strong>{studio}</strong></td>
|
||||||
<td>The studio for the media item.</td>
|
<td>The studio for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{content_rating}</strong></td>
|
<td><strong>{content_rating}</strong></td>
|
||||||
<td>The content rating for the media item. (e.g. TV-MA, TV-PG, etc.)</td>
|
<td>The content rating for the item. <span class="small-muted">(e.g. TV-MA, TV-PG, etc.)</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{directors}</strong></td>
|
<td><strong>{directors}</strong></td>
|
||||||
<td>A list of directors for the media item.</td>
|
<td>A list of directors for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{writers}</strong></td>
|
<td><strong>{writers}</strong></td>
|
||||||
<td>A list of writers for the media item.</td>
|
<td>A list of writers for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{actors}</strong></td>
|
<td><strong>{actors}</strong></td>
|
||||||
<td>A list of actors for the media item.</td>
|
<td>A list of actors for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{genres}</strong></td>
|
<td><strong>{genres}</strong></td>
|
||||||
<td>A list of genres for the media item.</td>
|
<td>A list of genres for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{summary}</strong></td>
|
<td><strong>{summary}</strong></td>
|
||||||
<td>A short plot summary for the media item.</td>
|
<td>A short plot summary for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{tagline}</strong></td>
|
<td><strong>{tagline}</strong></td>
|
||||||
@@ -1323,6 +1485,65 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<td><strong>{duration}</strong></td>
|
<td><strong>{duration}</strong></td>
|
||||||
<td>The duration (in minutes) for the item.</td>
|
<td>The duration (in minutes) for the item.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{poster_url}</strong></td>
|
||||||
|
<td>A URL for the movie, TV show, or album poster.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{imdb_id}</strong></td>
|
||||||
|
<td>The IMDB ID for the movie. <span class="small-muted">(e.g. tt2488496)</span>
|
||||||
|
<p class="small-muted">(PMS agent must be Freebase)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{imdb_url}</strong></td>
|
||||||
|
<td>The IMDB URL for the movie.
|
||||||
|
<p class="small-muted">(PMS agent must be Freebase)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{thetvdb_id}</strong></td>
|
||||||
|
<td>The TVDB ID for the TV show. <span class="small-muted">(e.g. 121361)</span>
|
||||||
|
<p class="small-muted">(PMS agent must be TheTVDB)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{thetvdb_url}</strong></td>
|
||||||
|
<td>The TVDB URL for the TV show.
|
||||||
|
<p class="small-muted">(PMS agent must be TheTVDB)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{themoviedb_id}</strong></td>
|
||||||
|
<td>The TMDb ID for the movie or TV show. <span class="small-muted">(e.g. 15260)</span>
|
||||||
|
<p class="small-muted">(PMS agent must be The Movie Database)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{themoviedb_url}</strong></td>
|
||||||
|
<td>The TMDb URL for the movie or TV show.
|
||||||
|
<p class="small-muted">(PMS agent must be The Movie Database)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{lastfm_url}</strong></td>
|
||||||
|
<td>The last.fm URL for the album.
|
||||||
|
<p class="small-muted">(PMS agent must be Last.fm)</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{trakt_url}</strong></td>
|
||||||
|
<td>The trakt.tv URL for the movie or TV show.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{section_id}</strong></td>
|
||||||
|
<td>The unique identifier for the library.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{rating_key}</strong></td>
|
||||||
|
<td>The unique identifier for the movie, episode, or track.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{parent_rating_key}</strong></td>
|
||||||
|
<td>The unique identifier for the season or album.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{grandparent_rating_key}</strong></td>
|
||||||
|
<td>The unique identifier for the TV show or artist.</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -1362,7 +1583,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<div>
|
<div>
|
||||||
<p class="help-block">All text inside a <strong>music</strong> tag will only be sent when the media item being played back is a music track.</p>
|
<p class="help-block">All text inside a <strong>music</strong> tag will only be sent when the media item being played back is a music track.</p>
|
||||||
<p><strong style="color: #fff;">Example:</strong></p>
|
<p><strong style="color: #fff;">Example:</strong></p>
|
||||||
<pre>{user} has started playing {title} <music>(Track {episode_num})</music></pre>
|
<pre>{user} has started playing {title} <music>(Track {track_num})</music></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1408,6 +1629,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
<script src="interfaces/default/js/parsley.min.js"></script>
|
<script src="interfaces/default/js/parsley.min.js"></script>
|
||||||
<script src="interfaces/default/js/Sortable.min.js"></script>
|
<script src="interfaces/default/js/Sortable.min.js"></script>
|
||||||
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
@@ -1439,7 +1661,7 @@ $(document).ready(function() {
|
|||||||
var configForm = $("#configUpdate");
|
var configForm = $("#configUpdate");
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
if (configForm.parsley().validate()) {
|
if (configForm.parsley().validate()) {
|
||||||
doAjaxCall('configUpdate', $(this), 'tabs', true);
|
doAjaxCall('configUpdate', $(this), 'tabs', true, getSchedulerTable);
|
||||||
postSaveChecks();
|
postSaveChecks();
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -1518,6 +1740,20 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($("#https_create_cert").is(":checked")) {
|
||||||
|
$("#https_options_self-signed").show();
|
||||||
|
} else {
|
||||||
|
$("#https_options_self-signed").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#https_create_cert").click(function(){
|
||||||
|
if ($("#https_create_cert").is(":checked")) {
|
||||||
|
$("#https_options_self-signed").slideDown();
|
||||||
|
} else {
|
||||||
|
$("#https_options_self-signed").slideUp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$( ".http-settings" ).change(function() {
|
$( ".http-settings" ).change(function() {
|
||||||
httpChanged = true;
|
httpChanged = true;
|
||||||
});
|
});
|
||||||
@@ -1534,15 +1770,16 @@ $(document).ready(function() {
|
|||||||
serverChanged = true;
|
serverChanged = true;
|
||||||
$("#pms_identifier").val("");
|
$("#pms_identifier").val("");
|
||||||
$("#pms-verify-status").html("");
|
$("#pms-verify-status").html("");
|
||||||
|
$("#server_changed").prop('checked', true);
|
||||||
verifyServer();
|
verifyServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
function verifyServer(_callback) {
|
function verifyServer(_callback) {
|
||||||
var pms_ip = $("#pms_ip").val()
|
var pms_ip = $("#pms_ip").val();
|
||||||
var pms_port = $("#pms_port").val()
|
var pms_port = $("#pms_port").val();
|
||||||
var pms_identifier = $("#pms_identifier").val()
|
var pms_identifier = $("#pms_identifier").val();
|
||||||
var pms_ssl = $("#pms_ssl").val()
|
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||||
var pms_is_remote = $("#pms_is_remote").val()
|
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||||
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
||||||
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>');
|
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>');
|
||||||
$('#pms-verify').fadeIn('fast');
|
$('#pms-verify').fadeIn('fast');
|
||||||
@@ -1551,15 +1788,16 @@ $(document).ready(function() {
|
|||||||
data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
|
data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
|
||||||
cache: true,
|
cache: true,
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 5000,
|
timeout: 10000,
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
$("#pms-verify").html('<i class="fa fa-close"></i>');
|
$("#pms-verify").html('<i class="fa fa-close"></i>');
|
||||||
$('#pms-verify').fadeIn('fast');
|
$('#pms-verify').fadeIn('fast');
|
||||||
$("#pms-ip-group").addClass("has-error");
|
$("#pms-ip-group").addClass("has-error");
|
||||||
},
|
},
|
||||||
success: function (xml) {
|
success: function (json) {
|
||||||
if ($(xml).find('MediaContainer').attr('machineIdentifier')) {
|
var machine_identifier = json;
|
||||||
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier'));
|
if (machine_identifier) {
|
||||||
|
$("#pms_identifier").val(machine_identifier);
|
||||||
$("#pms-verify").html('<i class="fa fa-check"></i>');
|
$("#pms-verify").html('<i class="fa fa-check"></i>');
|
||||||
$('#pms-verify').fadeIn('fast');
|
$('#pms-verify').fadeIn('fast');
|
||||||
$("#pms-ip-group").removeClass("has-error");
|
$("#pms-ip-group").removeClass("has-error");
|
||||||
@@ -1630,10 +1868,10 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Load notification agent config modal
|
// Load notification agent config modal
|
||||||
$(".toggle-notification-config-modal").click(function() {
|
$(".toggle-notification-config-modal").click(function() {
|
||||||
var configId = $(this).data('id');
|
var agent_id = $(this).data('id');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_notification_agent_config',
|
url: 'get_notification_agent_config',
|
||||||
data: { config_id: configId },
|
data: { agent_id: agent_id },
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
@@ -1644,10 +1882,10 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Load notification triggers config modal
|
// Load notification triggers config modal
|
||||||
$(".toggle-notification-triggers-modal").click(function() {
|
$(".toggle-notification-triggers-modal").click(function() {
|
||||||
var configId = $(this).data('id');
|
var agent_id = $(this).data('id');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_notification_agent_triggers',
|
url: 'get_notification_agent_triggers',
|
||||||
data: { config_id: configId },
|
data: { agent_id: agent_id },
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
@@ -1662,33 +1900,61 @@ $(document).ready(function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_server_pref',
|
url: 'get_server_identity',
|
||||||
data: { pref: 'logDebug' },
|
|
||||||
async: true,
|
async: true,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
if (data !== 'true') {
|
var version = data.version.split('.')
|
||||||
$("#debugLogCheck").html("Debug logging must be enabled on your Plex Server. <a target='_blank' href='https://support.plex.tv/hc/en-us/articles/201643703-Reporting-issues-with-Plex-Media-Server'> More..</a>");
|
if (parseInt(version[0]) >= 0 && parseInt(version[1]) >= 9 && parseInt(version[2]) >= 14) {
|
||||||
|
$("#debugLogCheck").html("IP address is automatically logged for PMS version 0.9.14 and above.");
|
||||||
$("#ip_logging_enable").attr("disabled", true);
|
$("#ip_logging_enable").attr("disabled", true);
|
||||||
|
$("#ip_logging_enable").attr("checked", true);
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_server_pref',
|
||||||
|
data: { pref: 'logDebug' },
|
||||||
|
async: true,
|
||||||
|
success: function(data) {
|
||||||
|
if (data !== 'true') {
|
||||||
|
$("#debugLogCheck").html("Debug logging must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/201643703-Reporting-issues-with-Plex-Media-Server')}'>Click here</a> for help.");
|
||||||
|
$("#ip_logging_enable").attr("disabled", true);
|
||||||
|
$("#ip_logging_enable").attr("checked", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check to see if our logs folder is set before allowing IP logging to be enabled.
|
||||||
|
checkLogsPath();
|
||||||
|
|
||||||
|
$("#pms_logs_folder").change(function() {
|
||||||
|
checkLogsPath();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkLogsPath() {
|
||||||
|
if ($("#pms_logs_folder").val() == '') {
|
||||||
|
$("#debugLogCheck").html("You must first define your Plex Server Logs folder path under the Plex Media Server tab.");
|
||||||
|
$("#ip_logging_enable").attr("disabled", true);
|
||||||
|
$("#ip_logging_enable").attr("checked", false);
|
||||||
|
} else {
|
||||||
|
$("#ip_logging_enable").attr("disabled", false);
|
||||||
|
$("#debugLogCheck").html("");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check to see if our logs folder is set before allowing IP logging to be enabled.
|
$.ajax({
|
||||||
checkLogsPath();
|
url: 'get_server_pref',
|
||||||
|
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||||
$("#pms_logs_folder").change(function() {
|
async: true,
|
||||||
checkLogsPath();
|
success: function(data) {
|
||||||
});
|
if (data !== 'true') {
|
||||||
|
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||||
function checkLogsPath() {
|
$("#monitor_remote_access").attr("disabled", true);
|
||||||
if ($("#pms_logs_folder").val() == '') {
|
$("#monitor_remote_access").attr("checked", false);
|
||||||
$("#debugLogCheck").html("You must first define your Plex Server Logs folder path under the Plex Media Server tab.");
|
}
|
||||||
$("#ip_logging_enable").attr("disabled", true);
|
|
||||||
} else {
|
|
||||||
$("#ip_logging_enable").attr("disabled", false);
|
|
||||||
$("#debugLogCheck").html("");
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
var accordion_session = new Accordion($('#accordion-session'), false);
|
var accordion_session = new Accordion($('#accordion-session'), false);
|
||||||
var accordion_timeline = new Accordion($('#accordion-timeline'), false);
|
var accordion_timeline = new Accordion($('#accordion-timeline'), false);
|
||||||
@@ -1776,6 +2042,19 @@ $(document).ready(function() {
|
|||||||
};
|
};
|
||||||
$(this).on('focus keyup input', function() { resizeTextarea(this); }).removeAttr('data-autoresize');
|
$(this).on('focus keyup input', function() { resizeTextarea(this); }).removeAttr('data-autoresize');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getSchedulerTable() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_scheduler_table',
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#plexpy-scheduler-table").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getSchedulerTable();
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
|
||||||
|
<link rel="stylesheet" href="interfaces/default/css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
<span><i class="fa fa-group"></i> All Users</span>
|
<span><i class="fa fa-group"></i> All Users</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
|
<div class="colvis-button-bar hidden-xs"></div>
|
||||||
<button class="btn btn-dark refresh-users-button" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
|
<button class="btn btn-dark refresh-users-button" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
|
||||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||||
<i class="fa fa-pencil"></i> Edit mode
|
<i class="fa fa-pencil"></i> Edit mode
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
<th align="left" id="last_player">Last Player</th>
|
<th align="left" id="last_player">Last Player</th>
|
||||||
<th align="left" id="last_played">Last Played</th>
|
<th align="left" id="last_played">Last Played</th>
|
||||||
<th align="left" id="total_plays">Total Plays</th>
|
<th align="left" id="total_plays">Total Plays</th>
|
||||||
|
<th align="left" id="total_duration">Total Duration</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -67,6 +70,7 @@
|
|||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="interfaces/default/js/dataTables.colVis.js"></script>
|
||||||
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
||||||
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
@@ -84,6 +88,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
|
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
|
||||||
|
var colvis = new $.fn.dataTable.ColVis(users_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
|
||||||
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
clearSearchButton('users_list_table', users_list_table);
|
clearSearchButton('users_list_table', users_list_table);
|
||||||
|
|
||||||
|
@@ -83,7 +83,7 @@ from plexpy import common
|
|||||||
<div class="col-xs-4">
|
<div class="col-xs-4">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Force SSL
|
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Use SSL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +169,7 @@ from plexpy import common
|
|||||||
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" ${config['launch_browser']}>
|
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" ${config['launch_browser']}>
|
||||||
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" ${config['refresh_users_on_startup']}>
|
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" ${config['refresh_users_on_startup']}>
|
||||||
<input type="checkbox" name="refresh_libraries_on_startup" id="refresh_libraries_on_startup" value="1" ${config['refresh_libraries_on_startup']}>
|
<input type="checkbox" name="refresh_libraries_on_startup" id="refresh_libraries_on_startup" value="1" ${config['refresh_libraries_on_startup']}>
|
||||||
|
<input type="checkbox" name="server_changed" id="server_changed" value="1" checked>
|
||||||
<input type="checkbox" name="first_run_complete" id="first_run_complete" value="1" checked>
|
<input type="checkbox" name="first_run_complete" id="first_run_complete" value="1" checked>
|
||||||
<input type="checkbox" name="check_github" id="check_github" value="1" checked>
|
<input type="checkbox" name="check_github" id="check_github" value="1" checked>
|
||||||
<input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
|
<input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
|
||||||
@@ -243,7 +244,7 @@ from plexpy import common
|
|||||||
},
|
},
|
||||||
render: {
|
render: {
|
||||||
option: function (item, escape) {
|
option: function (item, escape) {
|
||||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
|
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
||||||
},
|
},
|
||||||
item: function (item, escape) {
|
item: function (item, escape) {
|
||||||
// first item is rendered before initialization bug?
|
// first item is rendered before initialization bug?
|
||||||
@@ -253,7 +254,7 @@ from plexpy import common
|
|||||||
.filter('[value="' + item.value + '"]').data());
|
.filter('[value="' + item.value + '"]').data());
|
||||||
|
|
||||||
}
|
}
|
||||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
|
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChange: function (item) {
|
onChange: function (item) {
|
||||||
@@ -377,8 +378,8 @@ from plexpy import common
|
|||||||
var pms_ip = $("#pms_ip").val().trim();
|
var pms_ip = $("#pms_ip").val().trim();
|
||||||
var pms_port = $("#pms_port").val().trim();
|
var pms_port = $("#pms_port").val().trim();
|
||||||
var pms_identifier = $("#pms_identifier").val();
|
var pms_identifier = $("#pms_identifier").val();
|
||||||
var pms_ssl = $("#pms_ssl").val();
|
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||||
var pms_is_remote = $("#pms_is_remote").val();
|
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
@@ -392,9 +393,10 @@ from plexpy import common
|
|||||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
|
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
},
|
},
|
||||||
success: function (xml) {
|
success: function (json) {
|
||||||
if ($(xml).find('MediaContainer').attr('machineIdentifier')) {
|
var machine_identifier = json;
|
||||||
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier'));
|
if (machine_identifier) {
|
||||||
|
$("#pms_identifier").val(machine_identifier);
|
||||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
pms_verified = true;
|
pms_verified = true;
|
||||||
|
@@ -38,7 +38,7 @@ status_cmd="${name}_status"
|
|||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
command="/usr/sbin/daemon"
|
command="/usr/sbin/daemon"
|
||||||
command_args="-f -p ${plexpy_pid} python ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
|
command_args="python2 ${plexpy_dir}/PlexPy.py --daemon --pidfile ${plexpy_pid} --quiet --nolaunch"
|
||||||
|
|
||||||
# Ensure user is root when running this script.
|
# Ensure user is root when running this script.
|
||||||
if [ `id -u` != "0" ]; then
|
if [ `id -u` != "0" ]; then
|
||||||
|
@@ -38,7 +38,7 @@ status_cmd="${name}_status"
|
|||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
command="/usr/sbin/daemon"
|
command="/usr/sbin/daemon"
|
||||||
command_args="-f -p ${plexpy_pid} python2 ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
|
command_args="python2 ${plexpy_dir}/PlexPy.py --daemon --pidfile ${plexpy_pid} --quiet --nolaunch"
|
||||||
|
|
||||||
# Ensure user is root when running this script.
|
# Ensure user is root when running this script.
|
||||||
if [ `id -u` != "0" ]; then
|
if [ `id -u` != "0" ]; then
|
||||||
|
1652
lib/IPy.py
Normal file
1652
lib/IPy.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,21 @@
|
|||||||
# -*- coding: latin-1 -*-
|
# -*- coding: latin-1 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) Martin Sj<53>gren and AB Strakt 2001, All rights reserved
|
# Copyright (C) AB Strakt
|
||||||
# Copyright (C) Jean-Paul Calderone 2008, All rights reserved
|
# Copyright (C) Jean-Paul Calderone
|
||||||
# This file is licenced under the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 or later (aka LGPL v2.1)
|
# See LICENSE for details.
|
||||||
# Please see LGPL2.1.txt for more information
|
|
||||||
"""
|
"""
|
||||||
Certificate generation module.
|
Certificate generation module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
import time
|
|
||||||
|
|
||||||
TYPE_RSA = crypto.TYPE_RSA
|
TYPE_RSA = crypto.TYPE_RSA
|
||||||
TYPE_DSA = crypto.TYPE_DSA
|
TYPE_DSA = crypto.TYPE_DSA
|
||||||
|
|
||||||
serial = int(time.time())
|
|
||||||
|
|
||||||
|
|
||||||
def createKeyPair(type, bits):
|
def createKeyPair(type, bits):
|
||||||
"""
|
"""
|
||||||
Create a public/private key pair.
|
Create a public/private key pair.
|
||||||
|
|
||||||
Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
|
Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
|
||||||
bits - Number of bits to use in the key
|
bits - Number of bits to use in the key
|
||||||
Returns: The public/private key pair in a PKey object
|
Returns: The public/private key pair in a PKey object
|
||||||
@@ -29,12 +24,11 @@ def createKeyPair(type, bits):
|
|||||||
pkey.generate_key(type, bits)
|
pkey.generate_key(type, bits)
|
||||||
return pkey
|
return pkey
|
||||||
|
|
||||||
def createCertRequest(pkey, digest="md5", **name):
|
def createCertRequest(pkey, digest="sha256", **name):
|
||||||
"""
|
"""
|
||||||
Create a certificate request.
|
Create a certificate request.
|
||||||
|
|
||||||
Arguments: pkey - The key to associate with the request
|
Arguments: pkey - The key to associate with the request
|
||||||
digest - Digestion method to use for signing, default is md5
|
digest - Digestion method to use for signing, default is sha256
|
||||||
**name - The name of the subject of the request, possible
|
**name - The name of the subject of the request, possible
|
||||||
arguments are:
|
arguments are:
|
||||||
C - Country name
|
C - Country name
|
||||||
@@ -49,18 +43,17 @@ def createCertRequest(pkey, digest="md5", **name):
|
|||||||
req = crypto.X509Req()
|
req = crypto.X509Req()
|
||||||
subj = req.get_subject()
|
subj = req.get_subject()
|
||||||
|
|
||||||
for (key,value) in name.items():
|
for key, value in name.items():
|
||||||
setattr(subj, key, value)
|
setattr(subj, key, value)
|
||||||
|
|
||||||
req.set_pubkey(pkey)
|
req.set_pubkey(pkey)
|
||||||
req.sign(pkey, digest)
|
req.sign(pkey, digest)
|
||||||
return req
|
return req
|
||||||
|
|
||||||
def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"):
|
def createCertificate(req, issuerCertKey, serial, validityPeriod, digest="sha256"):
|
||||||
"""
|
"""
|
||||||
Generate a certificate given a certificate request.
|
Generate a certificate given a certificate request.
|
||||||
|
Arguments: req - Certificate request to use
|
||||||
Arguments: req - Certificate reqeust to use
|
|
||||||
issuerCert - The certificate of the issuer
|
issuerCert - The certificate of the issuer
|
||||||
issuerKey - The private key of the issuer
|
issuerKey - The private key of the issuer
|
||||||
serial - Serial number for the certificate
|
serial - Serial number for the certificate
|
||||||
@@ -68,9 +61,11 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
|
|||||||
starts being valid
|
starts being valid
|
||||||
notAfter - Timestamp (relative to now) when the certificate
|
notAfter - Timestamp (relative to now) when the certificate
|
||||||
stops being valid
|
stops being valid
|
||||||
digest - Digest method to use for signing, default is md5
|
digest - Digest method to use for signing, default is sha256
|
||||||
Returns: The signed certificate in an X509 object
|
Returns: The signed certificate in an X509 object
|
||||||
"""
|
"""
|
||||||
|
issuerCert, issuerKey = issuerCertKey
|
||||||
|
notBefore, notAfter = validityPeriod
|
||||||
cert = crypto.X509()
|
cert = crypto.X509()
|
||||||
cert.set_serial_number(serial)
|
cert.set_serial_number(serial)
|
||||||
cert.gmtime_adj_notBefore(notBefore)
|
cert.gmtime_adj_notBefore(notBefore)
|
||||||
@@ -80,3 +75,32 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
|
|||||||
cert.set_pubkey(req.get_pubkey())
|
cert.set_pubkey(req.get_pubkey())
|
||||||
cert.sign(issuerKey, digest)
|
cert.sign(issuerKey, digest)
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
|
def createSelfSignedCertificate((issuerName, issuerKey), serial, (notBefore, notAfter), altNames, digest="sha256"):
|
||||||
|
"""
|
||||||
|
Generate a certificate given a certificate request.
|
||||||
|
Arguments: issuerName - The name of the issuer
|
||||||
|
issuerKey - The private key of the issuer
|
||||||
|
serial - Serial number for the certificate
|
||||||
|
notBefore - Timestamp (relative to now) when the certificate
|
||||||
|
starts being valid
|
||||||
|
notAfter - Timestamp (relative to now) when the certificate
|
||||||
|
stops being valid
|
||||||
|
altNames - The alternative names
|
||||||
|
digest - Digest method to use for signing, default is sha256
|
||||||
|
Returns: The signed certificate in an X509 object
|
||||||
|
"""
|
||||||
|
cert = crypto.X509()
|
||||||
|
cert.set_version(2)
|
||||||
|
cert.set_serial_number(serial)
|
||||||
|
cert.get_subject().CN = issuerName
|
||||||
|
cert.gmtime_adj_notBefore(notBefore)
|
||||||
|
cert.gmtime_adj_notAfter(notAfter)
|
||||||
|
cert.set_issuer(cert.get_subject())
|
||||||
|
cert.set_pubkey(issuerKey)
|
||||||
|
|
||||||
|
if altNames:
|
||||||
|
cert.add_extensions([crypto.X509Extension("subjectAltName", False, altNames)])
|
||||||
|
|
||||||
|
cert.sign(issuerKey, digest)
|
||||||
|
return cert
|
||||||
|
732
lib/profilehooks.py
Normal file
732
lib/profilehooks.py
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
"""
|
||||||
|
Profiling hooks
|
||||||
|
|
||||||
|
This module contains a couple of decorators (`profile` and `coverage`) that
|
||||||
|
can be used to wrap functions and/or methods to produce profiles and line
|
||||||
|
coverage reports. There's a third convenient decorator (`timecall`) that
|
||||||
|
measures the duration of function execution without the extra profiling
|
||||||
|
overhead.
|
||||||
|
|
||||||
|
Usage example (Python 2.4 or newer)::
|
||||||
|
|
||||||
|
from profilehooks import profile, coverage
|
||||||
|
|
||||||
|
@profile # or @coverage
|
||||||
|
def fn(n):
|
||||||
|
if n < 2: return 1
|
||||||
|
else: return n * fn(n-1)
|
||||||
|
|
||||||
|
print fn(42)
|
||||||
|
|
||||||
|
Usage example (Python 2.3 or older)::
|
||||||
|
|
||||||
|
from profilehooks import profile, coverage
|
||||||
|
|
||||||
|
def fn(n):
|
||||||
|
if n < 2: return 1
|
||||||
|
else: return n * fn(n-1)
|
||||||
|
|
||||||
|
# Now wrap that function in a decorator
|
||||||
|
fn = profile(fn) # or coverage(fn)
|
||||||
|
|
||||||
|
print fn(42)
|
||||||
|
|
||||||
|
Reports for all thusly decorated functions will be printed to sys.stdout
|
||||||
|
on program termination. You can alternatively request for immediate
|
||||||
|
reports for each call by passing immediate=True to the profile decorator.
|
||||||
|
|
||||||
|
There's also a @timecall decorator for printing the time to sys.stderr
|
||||||
|
every time a function is called, when you just want to get a rough measure
|
||||||
|
instead of a detailed (but costly) profile.
|
||||||
|
|
||||||
|
Caveats
|
||||||
|
|
||||||
|
A thread on python-dev convinced me that hotshot produces bogus numbers.
|
||||||
|
See http://mail.python.org/pipermail/python-dev/2005-November/058264.html
|
||||||
|
|
||||||
|
I don't know what will happen if a decorated function will try to call
|
||||||
|
another decorated function. All decorators probably need to explicitly
|
||||||
|
support nested profiling (currently TraceFuncCoverage is the only one
|
||||||
|
that supports this, while HotShotFuncProfile has support for recursive
|
||||||
|
functions.)
|
||||||
|
|
||||||
|
Profiling with hotshot creates temporary files (*.prof for profiling,
|
||||||
|
*.cprof for coverage) in the current directory. These files are not
|
||||||
|
cleaned up. Exception: when you specify a filename to the profile
|
||||||
|
decorator (to store the pstats.Stats object for later inspection),
|
||||||
|
the temporary file will be the filename you specified with '.raw'
|
||||||
|
appended at the end.
|
||||||
|
|
||||||
|
Coverage analysis with hotshot seems to miss some executions resulting
|
||||||
|
in lower line counts and some lines errorneously marked as never
|
||||||
|
executed. For this reason coverage analysis now uses trace.py which is
|
||||||
|
slower, but more accurate.
|
||||||
|
|
||||||
|
Copyright (c) 2004--2008 Marius Gedminas <marius@pov.lt>
|
||||||
|
Copyright (c) 2007 Hanno Schlichting
|
||||||
|
Copyright (c) 2008 Florian Schulze
|
||||||
|
|
||||||
|
Released under the MIT licence since December 2006:
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
(Previously it was distributed under the GNU General Public Licence.)
|
||||||
|
"""
|
||||||
|
# $Id: profilehooks.py 29 2010-08-13 16:29:20Z mg $
|
||||||
|
|
||||||
|
__author__ = "Marius Gedminas (marius@gedmin.as)"
|
||||||
|
__copyright__ = "Copyright 2004-2009 Marius Gedminas"
|
||||||
|
__license__ = "MIT"
|
||||||
|
__version__ = "1.4"
|
||||||
|
__date__ = "2009-03-31"
|
||||||
|
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
# For profiling
|
||||||
|
from profile import Profile
|
||||||
|
import pstats
|
||||||
|
|
||||||
|
# For hotshot profiling (inaccurate!)
|
||||||
|
try:
|
||||||
|
import hotshot
|
||||||
|
import hotshot.stats
|
||||||
|
except ImportError:
|
||||||
|
hotshot = None
|
||||||
|
|
||||||
|
# For trace.py coverage
|
||||||
|
import trace
|
||||||
|
|
||||||
|
# For hotshot coverage (inaccurate!; uses undocumented APIs; might break)
|
||||||
|
if hotshot is not None:
|
||||||
|
import _hotshot
|
||||||
|
import hotshot.log
|
||||||
|
|
||||||
|
# For cProfile profiling (best)
|
||||||
|
try:
|
||||||
|
import cProfile
|
||||||
|
except ImportError:
|
||||||
|
cProfile = None
|
||||||
|
|
||||||
|
# For timecall
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
# registry of available profilers
|
||||||
|
AVAILABLE_PROFILERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False,
|
||||||
|
sort=None, entries=40,
|
||||||
|
profiler=('cProfile', 'profile', 'hotshot')):
|
||||||
|
"""Mark `fn` for profiling.
|
||||||
|
|
||||||
|
If `skip` is > 0, first `skip` calls to `fn` will not be profiled.
|
||||||
|
|
||||||
|
If `immediate` is False, profiling results will be printed to
|
||||||
|
sys.stdout on program termination. Otherwise results will be printed
|
||||||
|
after each call.
|
||||||
|
|
||||||
|
If `dirs` is False only the name of the file will be printed.
|
||||||
|
Otherwise the full path is used.
|
||||||
|
|
||||||
|
`sort` can be a list of sort keys (defaulting to ['cumulative',
|
||||||
|
'time', 'calls']). The following ones are recognized::
|
||||||
|
|
||||||
|
'calls' -- call count
|
||||||
|
'cumulative' -- cumulative time
|
||||||
|
'file' -- file name
|
||||||
|
'line' -- line number
|
||||||
|
'module' -- file name
|
||||||
|
'name' -- function name
|
||||||
|
'nfl' -- name/file/line
|
||||||
|
'pcalls' -- call count
|
||||||
|
'stdname' -- standard name
|
||||||
|
'time' -- internal time
|
||||||
|
|
||||||
|
`entries` limits the output to the first N entries.
|
||||||
|
|
||||||
|
`profiler` can be used to select the preferred profiler, or specify a
|
||||||
|
sequence of them, in order of preference. The default is ('cProfile'.
|
||||||
|
'profile', 'hotshot').
|
||||||
|
|
||||||
|
If `filename` is specified, the profile stats will be stored in the
|
||||||
|
named file. You can load them pstats.Stats(filename).
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
def fn(...):
|
||||||
|
...
|
||||||
|
fn = profile(fn, skip=1)
|
||||||
|
|
||||||
|
If you are using Python 2.4, you should be able to use the decorator
|
||||||
|
syntax::
|
||||||
|
|
||||||
|
@profile(skip=3)
|
||||||
|
def fn(...):
|
||||||
|
...
|
||||||
|
|
||||||
|
or just ::
|
||||||
|
|
||||||
|
@profile
|
||||||
|
def fn(...):
|
||||||
|
...
|
||||||
|
|
||||||
|
"""
|
||||||
|
if fn is None: # @profile() syntax -- we are a decorator maker
|
||||||
|
def decorator(fn):
|
||||||
|
return profile(fn, skip=skip, filename=filename,
|
||||||
|
immediate=immediate, dirs=dirs,
|
||||||
|
sort=sort, entries=entries,
|
||||||
|
profiler=profiler)
|
||||||
|
return decorator
|
||||||
|
# @profile syntax -- we are a decorator.
|
||||||
|
if isinstance(profiler, str):
|
||||||
|
profiler = [profiler]
|
||||||
|
for p in profiler:
|
||||||
|
if p in AVAILABLE_PROFILERS:
|
||||||
|
profiler_class = AVAILABLE_PROFILERS[p]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError('only these profilers are available: %s'
|
||||||
|
% ', '.join(AVAILABLE_PROFILERS))
|
||||||
|
fp = profiler_class(fn, skip=skip, filename=filename,
|
||||||
|
immediate=immediate, dirs=dirs,
|
||||||
|
sort=sort, entries=entries)
|
||||||
|
# fp = HotShotFuncProfile(fn, skip=skip, filename=filename, ...)
|
||||||
|
# or HotShotFuncProfile
|
||||||
|
# We cannot return fp or fp.__call__ directly as that would break method
|
||||||
|
# definitions, instead we need to return a plain function.
|
||||||
|
def new_fn(*args, **kw):
|
||||||
|
return fp(*args, **kw)
|
||||||
|
new_fn.__doc__ = fn.__doc__
|
||||||
|
new_fn.__name__ = fn.__name__
|
||||||
|
new_fn.__dict__ = fn.__dict__
|
||||||
|
new_fn.__module__ = fn.__module__
|
||||||
|
return new_fn
|
||||||
|
|
||||||
|
|
||||||
|
def coverage(fn):
|
||||||
|
"""Mark `fn` for line coverage analysis.
|
||||||
|
|
||||||
|
Results will be printed to sys.stdout on program termination.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
def fn(...):
|
||||||
|
...
|
||||||
|
fn = coverage(fn)
|
||||||
|
|
||||||
|
If you are using Python 2.4, you should be able to use the decorator
|
||||||
|
syntax::
|
||||||
|
|
||||||
|
@coverage
|
||||||
|
def fn(...):
|
||||||
|
...
|
||||||
|
|
||||||
|
"""
|
||||||
|
fp = TraceFuncCoverage(fn) # or HotShotFuncCoverage
|
||||||
|
# We cannot return fp or fp.__call__ directly as that would break method
|
||||||
|
# definitions, instead we need to return a plain function.
|
||||||
|
def new_fn(*args, **kw):
|
||||||
|
return fp(*args, **kw)
|
||||||
|
new_fn.__doc__ = fn.__doc__
|
||||||
|
new_fn.__name__ = fn.__name__
|
||||||
|
new_fn.__dict__ = fn.__dict__
|
||||||
|
new_fn.__module__ = fn.__module__
|
||||||
|
return new_fn
|
||||||
|
|
||||||
|
|
||||||
|
def coverage_with_hotshot(fn):
|
||||||
|
"""Mark `fn` for line coverage analysis.
|
||||||
|
|
||||||
|
Uses the 'hotshot' module for fast coverage analysis.
|
||||||
|
|
||||||
|
BUG: Produces inaccurate results.
|
||||||
|
|
||||||
|
See the docstring of `coverage` for usage examples.
|
||||||
|
"""
|
||||||
|
fp = HotShotFuncCoverage(fn)
|
||||||
|
# We cannot return fp or fp.__call__ directly as that would break method
|
||||||
|
# definitions, instead we need to return a plain function.
|
||||||
|
def new_fn(*args, **kw):
|
||||||
|
return fp(*args, **kw)
|
||||||
|
new_fn.__doc__ = fn.__doc__
|
||||||
|
new_fn.__name__ = fn.__name__
|
||||||
|
new_fn.__dict__ = fn.__dict__
|
||||||
|
new_fn.__module__ = fn.__module__
|
||||||
|
return new_fn
|
||||||
|
|
||||||
|
|
||||||
|
class FuncProfile(object):
|
||||||
|
"""Profiler for a function (uses profile)."""
|
||||||
|
|
||||||
|
# This flag is shared between all instances
|
||||||
|
in_profiler = False
|
||||||
|
|
||||||
|
Profile = Profile
|
||||||
|
|
||||||
|
def __init__(self, fn, skip=0, filename=None, immediate=False, dirs=False,
|
||||||
|
sort=None, entries=40):
|
||||||
|
"""Creates a profiler for a function.
|
||||||
|
|
||||||
|
Every profiler has its own log file (the name of which is derived
|
||||||
|
from the function name).
|
||||||
|
|
||||||
|
FuncProfile registers an atexit handler that prints profiling
|
||||||
|
information to sys.stderr when the program terminates.
|
||||||
|
"""
|
||||||
|
self.fn = fn
|
||||||
|
self.skip = skip
|
||||||
|
self.filename = filename
|
||||||
|
self.immediate = immediate
|
||||||
|
self.dirs = dirs
|
||||||
|
self.sort = sort or ('cumulative', 'time', 'calls')
|
||||||
|
if isinstance(self.sort, str):
|
||||||
|
self.sort = (self.sort, )
|
||||||
|
self.entries = entries
|
||||||
|
self.reset_stats()
|
||||||
|
atexit.register(self.atexit)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
"""Profile a singe call to the function."""
|
||||||
|
self.ncalls += 1
|
||||||
|
if self.skip > 0:
|
||||||
|
self.skip -= 1
|
||||||
|
self.skipped += 1
|
||||||
|
return self.fn(*args, **kw)
|
||||||
|
if FuncProfile.in_profiler:
|
||||||
|
# handle recursive calls
|
||||||
|
return self.fn(*args, **kw)
|
||||||
|
# You cannot reuse the same profiler for many calls and accumulate
|
||||||
|
# stats that way. :-/
|
||||||
|
profiler = self.Profile()
|
||||||
|
try:
|
||||||
|
FuncProfile.in_profiler = True
|
||||||
|
return profiler.runcall(self.fn, *args, **kw)
|
||||||
|
finally:
|
||||||
|
FuncProfile.in_profiler = False
|
||||||
|
self.stats.add(profiler)
|
||||||
|
if self.immediate:
|
||||||
|
self.print_stats()
|
||||||
|
self.reset_stats()
|
||||||
|
|
||||||
|
def print_stats(self):
|
||||||
|
"""Print profile information to sys.stdout."""
|
||||||
|
funcname = self.fn.__name__
|
||||||
|
filename = self.fn.func_code.co_filename
|
||||||
|
lineno = self.fn.func_code.co_firstlineno
|
||||||
|
print
|
||||||
|
print "*** PROFILER RESULTS ***"
|
||||||
|
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||||
|
print "function called %d times" % self.ncalls,
|
||||||
|
if self.skipped:
|
||||||
|
print "(%d calls not profiled)" % self.skipped
|
||||||
|
else:
|
||||||
|
print
|
||||||
|
print
|
||||||
|
stats = self.stats
|
||||||
|
if self.filename:
|
||||||
|
stats.dump_stats(self.filename)
|
||||||
|
if not self.dirs:
|
||||||
|
stats.strip_dirs()
|
||||||
|
stats.sort_stats(*self.sort)
|
||||||
|
stats.print_stats(self.entries)
|
||||||
|
|
||||||
|
def reset_stats(self):
|
||||||
|
"""Reset accumulated profiler statistics."""
|
||||||
|
# Note: not using self.Profile, since pstats.Stats() fails then
|
||||||
|
self.stats = pstats.Stats(Profile())
|
||||||
|
self.ncalls = 0
|
||||||
|
self.skipped = 0
|
||||||
|
|
||||||
|
def atexit(self):
|
||||||
|
"""Stop profiling and print profile information to sys.stdout.
|
||||||
|
|
||||||
|
This function is registered as an atexit hook.
|
||||||
|
"""
|
||||||
|
if not self.immediate:
|
||||||
|
self.print_stats()
|
||||||
|
|
||||||
|
|
||||||
|
AVAILABLE_PROFILERS['profile'] = FuncProfile
|
||||||
|
|
||||||
|
|
||||||
|
if cProfile is not None:
|
||||||
|
|
||||||
|
class CProfileFuncProfile(FuncProfile):
|
||||||
|
"""Profiler for a function (uses cProfile)."""
|
||||||
|
|
||||||
|
Profile = cProfile.Profile
|
||||||
|
|
||||||
|
AVAILABLE_PROFILERS['cProfile'] = CProfileFuncProfile
|
||||||
|
|
||||||
|
|
||||||
|
if hotshot is not None:
|
||||||
|
|
||||||
|
class HotShotFuncProfile(object):
|
||||||
|
"""Profiler for a function (uses hotshot)."""
|
||||||
|
|
||||||
|
# This flag is shared between all instances
|
||||||
|
in_profiler = False
|
||||||
|
|
||||||
|
def __init__(self, fn, skip=0, filename=None):
|
||||||
|
"""Creates a profiler for a function.
|
||||||
|
|
||||||
|
Every profiler has its own log file (the name of which is derived
|
||||||
|
from the function name).
|
||||||
|
|
||||||
|
HotShotFuncProfile registers an atexit handler that prints
|
||||||
|
profiling information to sys.stderr when the program terminates.
|
||||||
|
|
||||||
|
The log file is not removed and remains there to clutter the
|
||||||
|
current working directory.
|
||||||
|
"""
|
||||||
|
self.fn = fn
|
||||||
|
self.filename = filename
|
||||||
|
if self.filename:
|
||||||
|
self.logfilename = filename + ".raw"
|
||||||
|
else:
|
||||||
|
self.logfilename = fn.__name__ + ".prof"
|
||||||
|
self.profiler = hotshot.Profile(self.logfilename)
|
||||||
|
self.ncalls = 0
|
||||||
|
self.skip = skip
|
||||||
|
self.skipped = 0
|
||||||
|
atexit.register(self.atexit)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
"""Profile a singe call to the function."""
|
||||||
|
self.ncalls += 1
|
||||||
|
if self.skip > 0:
|
||||||
|
self.skip -= 1
|
||||||
|
self.skipped += 1
|
||||||
|
return self.fn(*args, **kw)
|
||||||
|
if HotShotFuncProfile.in_profiler:
|
||||||
|
# handle recursive calls
|
||||||
|
return self.fn(*args, **kw)
|
||||||
|
try:
|
||||||
|
HotShotFuncProfile.in_profiler = True
|
||||||
|
return self.profiler.runcall(self.fn, *args, **kw)
|
||||||
|
finally:
|
||||||
|
HotShotFuncProfile.in_profiler = False
|
||||||
|
|
||||||
|
def atexit(self):
|
||||||
|
"""Stop profiling and print profile information to sys.stderr.
|
||||||
|
|
||||||
|
This function is registered as an atexit hook.
|
||||||
|
"""
|
||||||
|
self.profiler.close()
|
||||||
|
funcname = self.fn.__name__
|
||||||
|
filename = self.fn.func_code.co_filename
|
||||||
|
lineno = self.fn.func_code.co_firstlineno
|
||||||
|
print
|
||||||
|
print "*** PROFILER RESULTS ***"
|
||||||
|
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||||
|
print "function called %d times" % self.ncalls,
|
||||||
|
if self.skipped:
|
||||||
|
print "(%d calls not profiled)" % self.skipped
|
||||||
|
else:
|
||||||
|
print
|
||||||
|
print
|
||||||
|
stats = hotshot.stats.load(self.logfilename)
|
||||||
|
# hotshot.stats.load takes ages, and the .prof file eats megabytes, but
|
||||||
|
# a saved stats object is small and fast
|
||||||
|
if self.filename:
|
||||||
|
stats.dump_stats(self.filename)
|
||||||
|
# it is best to save before strip_dirs
|
||||||
|
stats.strip_dirs()
|
||||||
|
stats.sort_stats('cumulative', 'time', 'calls')
|
||||||
|
stats.print_stats(40)
|
||||||
|
|
||||||
|
AVAILABLE_PROFILERS['hotshot'] = HotShotFuncProfile
|
||||||
|
|
||||||
|
|
||||||
|
class HotShotFuncCoverage:
|
||||||
|
"""Coverage analysis for a function (uses _hotshot).
|
||||||
|
|
||||||
|
HotShot coverage is reportedly faster than trace.py, but it appears to
|
||||||
|
have problems with exceptions; also line counts in coverage reports
|
||||||
|
are generally lower from line counts produced by TraceFuncCoverage.
|
||||||
|
Is this my bug, or is it a problem with _hotshot?
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fn):
|
||||||
|
"""Creates a profiler for a function.
|
||||||
|
|
||||||
|
Every profiler has its own log file (the name of which is derived
|
||||||
|
from the function name).
|
||||||
|
|
||||||
|
HotShotFuncCoverage registers an atexit handler that prints
|
||||||
|
profiling information to sys.stderr when the program terminates.
|
||||||
|
|
||||||
|
The log file is not removed and remains there to clutter the
|
||||||
|
current working directory.
|
||||||
|
"""
|
||||||
|
self.fn = fn
|
||||||
|
self.logfilename = fn.__name__ + ".cprof"
|
||||||
|
self.profiler = _hotshot.coverage(self.logfilename)
|
||||||
|
self.ncalls = 0
|
||||||
|
atexit.register(self.atexit)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
"""Profile a singe call to the function."""
|
||||||
|
self.ncalls += 1
|
||||||
|
return self.profiler.runcall(self.fn, args, kw)
|
||||||
|
|
||||||
|
def atexit(self):
|
||||||
|
"""Stop profiling and print profile information to sys.stderr.
|
||||||
|
|
||||||
|
This function is registered as an atexit hook.
|
||||||
|
"""
|
||||||
|
self.profiler.close()
|
||||||
|
funcname = self.fn.__name__
|
||||||
|
filename = self.fn.func_code.co_filename
|
||||||
|
lineno = self.fn.func_code.co_firstlineno
|
||||||
|
print
|
||||||
|
print "*** COVERAGE RESULTS ***"
|
||||||
|
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||||
|
print "function called %d times" % self.ncalls
|
||||||
|
print
|
||||||
|
fs = FuncSource(self.fn)
|
||||||
|
reader = hotshot.log.LogReader(self.logfilename)
|
||||||
|
for what, (filename, lineno, funcname), tdelta in reader:
|
||||||
|
if filename != fs.filename:
|
||||||
|
continue
|
||||||
|
if what == hotshot.log.LINE:
|
||||||
|
fs.mark(lineno)
|
||||||
|
if what == hotshot.log.ENTER:
|
||||||
|
# hotshot gives us the line number of the function definition
|
||||||
|
# and never gives us a LINE event for the first statement in
|
||||||
|
# a function, so if we didn't perform this mapping, the first
|
||||||
|
# statement would be marked as never executed
|
||||||
|
if lineno == fs.firstlineno:
|
||||||
|
lineno = fs.firstcodelineno
|
||||||
|
fs.mark(lineno)
|
||||||
|
reader.close()
|
||||||
|
print fs
|
||||||
|
|
||||||
|
|
||||||
|
class TraceFuncCoverage:
|
||||||
|
"""Coverage analysis for a function (uses trace module).
|
||||||
|
|
||||||
|
HotShot coverage analysis is reportedly faster, but it appears to have
|
||||||
|
problems with exceptions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Shared between all instances so that nested calls work
|
||||||
|
tracer = trace.Trace(count=True, trace=False,
|
||||||
|
ignoredirs=[sys.prefix, sys.exec_prefix])
|
||||||
|
|
||||||
|
# This flag is also shared between all instances
|
||||||
|
tracing = False
|
||||||
|
|
||||||
|
def __init__(self, fn):
|
||||||
|
"""Creates a profiler for a function.
|
||||||
|
|
||||||
|
Every profiler has its own log file (the name of which is derived
|
||||||
|
from the function name).
|
||||||
|
|
||||||
|
TraceFuncCoverage registers an atexit handler that prints
|
||||||
|
profiling information to sys.stderr when the program terminates.
|
||||||
|
|
||||||
|
The log file is not removed and remains there to clutter the
|
||||||
|
current working directory.
|
||||||
|
"""
|
||||||
|
self.fn = fn
|
||||||
|
self.logfilename = fn.__name__ + ".cprof"
|
||||||
|
self.ncalls = 0
|
||||||
|
atexit.register(self.atexit)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
"""Profile a singe call to the function."""
|
||||||
|
self.ncalls += 1
|
||||||
|
if TraceFuncCoverage.tracing:
|
||||||
|
return self.fn(*args, **kw)
|
||||||
|
try:
|
||||||
|
TraceFuncCoverage.tracing = True
|
||||||
|
return self.tracer.runfunc(self.fn, *args, **kw)
|
||||||
|
finally:
|
||||||
|
TraceFuncCoverage.tracing = False
|
||||||
|
|
||||||
|
def atexit(self):
|
||||||
|
"""Stop profiling and print profile information to sys.stderr.
|
||||||
|
|
||||||
|
This function is registered as an atexit hook.
|
||||||
|
"""
|
||||||
|
funcname = self.fn.__name__
|
||||||
|
filename = self.fn.func_code.co_filename
|
||||||
|
lineno = self.fn.func_code.co_firstlineno
|
||||||
|
print
|
||||||
|
print "*** COVERAGE RESULTS ***"
|
||||||
|
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||||
|
print "function called %d times" % self.ncalls
|
||||||
|
print
|
||||||
|
fs = FuncSource(self.fn)
|
||||||
|
for (filename, lineno), count in self.tracer.counts.items():
|
||||||
|
if filename != fs.filename:
|
||||||
|
continue
|
||||||
|
fs.mark(lineno, count)
|
||||||
|
print fs
|
||||||
|
never_executed = fs.count_never_executed()
|
||||||
|
if never_executed:
|
||||||
|
print "%d lines were not executed." % never_executed
|
||||||
|
|
||||||
|
|
||||||
|
class FuncSource:
|
||||||
|
"""Source code annotator for a function."""
|
||||||
|
|
||||||
|
blank_rx = re.compile(r"^\s*finally:\s*(#.*)?$")
|
||||||
|
|
||||||
|
def __init__(self, fn):
|
||||||
|
self.fn = fn
|
||||||
|
self.filename = inspect.getsourcefile(fn)
|
||||||
|
self.source, self.firstlineno = inspect.getsourcelines(fn)
|
||||||
|
self.sourcelines = {}
|
||||||
|
self.firstcodelineno = self.firstlineno
|
||||||
|
self.find_source_lines()
|
||||||
|
|
||||||
|
def find_source_lines(self):
|
||||||
|
"""Mark all executable source lines in fn as executed 0 times."""
|
||||||
|
strs = trace.find_strings(self.filename)
|
||||||
|
lines = trace.find_lines_from_code(self.fn.func_code, strs)
|
||||||
|
self.firstcodelineno = sys.maxint
|
||||||
|
for lineno in lines:
|
||||||
|
self.firstcodelineno = min(self.firstcodelineno, lineno)
|
||||||
|
self.sourcelines.setdefault(lineno, 0)
|
||||||
|
if self.firstcodelineno == sys.maxint:
|
||||||
|
self.firstcodelineno = self.firstlineno
|
||||||
|
|
||||||
|
def mark(self, lineno, count=1):
|
||||||
|
"""Mark a given source line as executed count times.
|
||||||
|
|
||||||
|
Multiple calls to mark for the same lineno add up.
|
||||||
|
"""
|
||||||
|
self.sourcelines[lineno] = self.sourcelines.get(lineno, 0) + count
|
||||||
|
|
||||||
|
def count_never_executed(self):
|
||||||
|
"""Count statements that were never executed."""
|
||||||
|
lineno = self.firstlineno
|
||||||
|
counter = 0
|
||||||
|
for line in self.source:
|
||||||
|
if self.sourcelines.get(lineno) == 0:
|
||||||
|
if not self.blank_rx.match(line):
|
||||||
|
counter += 1
|
||||||
|
lineno += 1
|
||||||
|
return counter
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return annotated source code for the function."""
|
||||||
|
lines = []
|
||||||
|
lineno = self.firstlineno
|
||||||
|
for line in self.source:
|
||||||
|
counter = self.sourcelines.get(lineno)
|
||||||
|
if counter is None:
|
||||||
|
prefix = ' ' * 7
|
||||||
|
elif counter == 0:
|
||||||
|
if self.blank_rx.match(line):
|
||||||
|
prefix = ' ' * 7
|
||||||
|
else:
|
||||||
|
prefix = '>' * 6 + ' '
|
||||||
|
else:
|
||||||
|
prefix = '%5d: ' % counter
|
||||||
|
lines.append(prefix + line)
|
||||||
|
lineno += 1
|
||||||
|
return ''.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def timecall(fn=None, immediate=True, timer=time.time):
|
||||||
|
"""Wrap `fn` and print its execution time.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
@timecall
|
||||||
|
def somefunc(x, y):
|
||||||
|
time.sleep(x * y)
|
||||||
|
|
||||||
|
somefunc(2, 3)
|
||||||
|
|
||||||
|
will print the time taken by somefunc on every call. If you want just
|
||||||
|
a summary at program termination, use
|
||||||
|
|
||||||
|
@timecall(immediate=False)
|
||||||
|
|
||||||
|
You can also choose a timing method other than the default ``time.time()``,
|
||||||
|
e.g.:
|
||||||
|
|
||||||
|
@timecall(timer=time.clock)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if fn is None: # @timecall() syntax -- we are a decorator maker
|
||||||
|
def decorator(fn):
|
||||||
|
return timecall(fn, immediate=immediate, timer=timer)
|
||||||
|
return decorator
|
||||||
|
# @timecall syntax -- we are a decorator.
|
||||||
|
fp = FuncTimer(fn, immediate=immediate, timer=timer)
|
||||||
|
# We cannot return fp or fp.__call__ directly as that would break method
|
||||||
|
# definitions, instead we need to return a plain function.
|
||||||
|
def new_fn(*args, **kw):
|
||||||
|
return fp(*args, **kw)
|
||||||
|
new_fn.__doc__ = fn.__doc__
|
||||||
|
new_fn.__name__ = fn.__name__
|
||||||
|
new_fn.__dict__ = fn.__dict__
|
||||||
|
new_fn.__module__ = fn.__module__
|
||||||
|
return new_fn
|
||||||
|
|
||||||
|
|
||||||
|
class FuncTimer(object):
|
||||||
|
|
||||||
|
def __init__(self, fn, immediate, timer):
|
||||||
|
self.fn = fn
|
||||||
|
self.ncalls = 0
|
||||||
|
self.totaltime = 0
|
||||||
|
self.immediate = immediate
|
||||||
|
self.timer = timer
|
||||||
|
if not immediate:
|
||||||
|
atexit.register(self.atexit)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
"""Profile a singe call to the function."""
|
||||||
|
fn = self.fn
|
||||||
|
timer = self.timer
|
||||||
|
self.ncalls += 1
|
||||||
|
try:
|
||||||
|
start = timer()
|
||||||
|
return fn(*args, **kw)
|
||||||
|
finally:
|
||||||
|
duration = timer() - start
|
||||||
|
self.totaltime += duration
|
||||||
|
if self.immediate:
|
||||||
|
funcname = fn.__name__
|
||||||
|
filename = fn.func_code.co_filename
|
||||||
|
lineno = fn.func_code.co_firstlineno
|
||||||
|
print >> sys.stderr, "\n %s (%s:%s):\n %.3f seconds\n" % (
|
||||||
|
funcname, filename, lineno, duration)
|
||||||
|
def atexit(self):
|
||||||
|
if not self.ncalls:
|
||||||
|
return
|
||||||
|
funcname = self.fn.__name__
|
||||||
|
filename = self.fn.func_code.co_filename
|
||||||
|
lineno = self.fn.func_code.co_firstlineno
|
||||||
|
print ("\n %s (%s:%s):\n"
|
||||||
|
" %d calls, %.3f seconds (%.3f seconds per call)\n" % (
|
||||||
|
funcname, filename, lineno, self.ncalls,
|
||||||
|
self.totaltime, self.totaltime / self.ncalls))
|
@@ -59,6 +59,7 @@ started = False
|
|||||||
DATA_DIR = None
|
DATA_DIR = None
|
||||||
|
|
||||||
CONFIG = None
|
CONFIG = None
|
||||||
|
CONFIG_FILE = None
|
||||||
|
|
||||||
DB_FILE = None
|
DB_FILE = None
|
||||||
|
|
||||||
@@ -73,17 +74,19 @@ UMASK = None
|
|||||||
|
|
||||||
POLLING_FAILOVER = False
|
POLLING_FAILOVER = False
|
||||||
|
|
||||||
|
|
||||||
def initialize(config_file):
|
def initialize(config_file):
|
||||||
with INIT_LOCK:
|
with INIT_LOCK:
|
||||||
|
|
||||||
global CONFIG
|
global CONFIG
|
||||||
|
global CONFIG_FILE
|
||||||
global _INITIALIZED
|
global _INITIALIZED
|
||||||
global CURRENT_VERSION
|
global CURRENT_VERSION
|
||||||
global LATEST_VERSION
|
global LATEST_VERSION
|
||||||
global UMASK
|
global UMASK
|
||||||
global POLLING_FAILOVER
|
global POLLING_FAILOVER
|
||||||
|
|
||||||
CONFIG = plexpy.config.Config(config_file)
|
CONFIG = plexpy.config.Config(config_file)
|
||||||
|
CONFIG_FILE = config_file
|
||||||
|
|
||||||
assert CONFIG is not None
|
assert CONFIG is not None
|
||||||
|
|
||||||
@@ -117,6 +120,15 @@ def initialize(config_file):
|
|||||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||||
verbose=VERBOSE)
|
verbose=VERBOSE)
|
||||||
|
|
||||||
|
if not CONFIG.BACKUP_DIR.startswith(os.path.abspath(DATA_DIR)):
|
||||||
|
# Put the backup dir in the data dir for now
|
||||||
|
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')
|
||||||
|
if not os.path.exists(CONFIG.BACKUP_DIR):
|
||||||
|
try:
|
||||||
|
os.makedirs(CONFIG.BACKUP_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("Could not create backup dir '%s': %s", BACKUP_DIR, e)
|
||||||
|
|
||||||
if not CONFIG.CACHE_DIR.startswith(os.path.abspath(DATA_DIR)):
|
if not CONFIG.CACHE_DIR.startswith(os.path.abspath(DATA_DIR)):
|
||||||
# Put the cache dir in the data dir for now
|
# Put the cache dir in the data dir for now
|
||||||
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
|
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
|
||||||
@@ -176,7 +188,7 @@ def initialize(config_file):
|
|||||||
plextv.refresh_users()
|
plextv.refresh_users()
|
||||||
|
|
||||||
# Refresh the libraries list on startup
|
# Refresh the libraries list on startup
|
||||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||||
pmsconnect.refresh_libraries()
|
pmsconnect.refresh_libraries()
|
||||||
|
|
||||||
# Store the original umask
|
# Store the original umask
|
||||||
@@ -186,7 +198,6 @@ def initialize(config_file):
|
|||||||
_INITIALIZED = True
|
_INITIALIZED = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def daemonize():
|
def daemonize():
|
||||||
if threading.activeCount() != 1:
|
if threading.activeCount() != 1:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -282,10 +293,10 @@ def initialize_scheduler():
|
|||||||
else:
|
else:
|
||||||
seconds = 0
|
seconds = 0
|
||||||
|
|
||||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.UPDATE_SECTION_IDS != -1:
|
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs',
|
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
|
||||||
hours=12, minutes=0, seconds=0)
|
hours=12, minutes=0, seconds=0)
|
||||||
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex Server Name',
|
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name',
|
||||||
hours=12, minutes=0, seconds=0)
|
hours=12, minutes=0, seconds=0)
|
||||||
|
|
||||||
if CONFIG.NOTIFY_RECENTLY_ADDED:
|
if CONFIG.NOTIFY_RECENTLY_ADDED:
|
||||||
@@ -296,10 +307,10 @@ def initialize_scheduler():
|
|||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
if CONFIG.MONITOR_REMOTE_ACCESS:
|
if CONFIG.MONITOR_REMOTE_ACCESS:
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
schedule_job(activity_pinger.check_server_response, 'Check for Plex remote access',
|
||||||
hours=0, minutes=0, seconds=seconds)
|
hours=0, minutes=0, seconds=seconds)
|
||||||
else:
|
else:
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
schedule_job(activity_pinger.check_server_response, 'Check for Plex remote access',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
# If we're not using websockets then fall back to polling
|
# If we're not using websockets then fall back to polling
|
||||||
@@ -322,6 +333,8 @@ def initialize_scheduler():
|
|||||||
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
|
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=hours, minutes=0, seconds=0)
|
hours=hours, minutes=0, seconds=0)
|
||||||
|
|
||||||
|
schedule_job(database.make_backup, 'Backup PlexPy database', hours=6, minutes=0, seconds=0, args=(True, True))
|
||||||
|
|
||||||
# Start scheduler
|
# Start scheduler
|
||||||
if start_jobs and len(SCHED.get_jobs()):
|
if start_jobs and len(SCHED.get_jobs()):
|
||||||
try:
|
try:
|
||||||
@@ -333,7 +346,7 @@ def initialize_scheduler():
|
|||||||
#SCHED.print_jobs()
|
#SCHED.print_jobs()
|
||||||
|
|
||||||
|
|
||||||
def schedule_job(function, name, hours=0, minutes=0, seconds=0):
|
def schedule_job(function, name, hours=0, minutes=0, seconds=0, args=None):
|
||||||
"""
|
"""
|
||||||
Start scheduled job if starting or restarting plexpy.
|
Start scheduled job if starting or restarting plexpy.
|
||||||
Reschedule job if Interval Settings have changed.
|
Reschedule job if Interval Settings have changed.
|
||||||
@@ -348,11 +361,11 @@ def schedule_job(function, name, hours=0, minutes=0, seconds=0):
|
|||||||
logger.info("Removed background task: %s", name)
|
logger.info("Removed background task: %s", name)
|
||||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||||
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
||||||
hours=hours, minutes=minutes, seconds=seconds))
|
hours=hours, minutes=minutes, seconds=seconds), args=args)
|
||||||
logger.info("Re-scheduled background task: %s", name)
|
logger.info("Re-scheduled background task: %s", name)
|
||||||
elif hours > 0 or minutes > 0 or seconds > 0:
|
elif hours > 0 or minutes > 0 or seconds > 0:
|
||||||
SCHED.add_job(function, id=name, trigger=IntervalTrigger(
|
SCHED.add_job(function, id=name, trigger=IntervalTrigger(
|
||||||
hours=hours, minutes=minutes, seconds=seconds))
|
hours=hours, minutes=minutes, seconds=seconds), args=args)
|
||||||
logger.info("Scheduled background task: %s", name)
|
logger.info("Scheduled background task: %s", name)
|
||||||
|
|
||||||
|
|
||||||
@@ -430,10 +443,10 @@ def dbcheck():
|
|||||||
|
|
||||||
# notify_log table :: This is a table which logs notifications sent
|
# notify_log table :: This is a table which logs notifications sent
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, '
|
||||||
'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
|
'session_key INTEGER, rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||||
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER, '
|
'user_id INTEGER, user TEXT, agent_id INTEGER, agent_name TEXT, notify_action TEXT, '
|
||||||
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER, on_created INTEGER)'
|
'subject_text TEXT, body_text TEXT, script_args TEXT, poster_url TEXT)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# library_sections table :: This table keeps record of the servers library sections
|
# library_sections table :: This table keeps record of the servers library sections
|
||||||
@@ -711,10 +724,57 @@ def dbcheck():
|
|||||||
'ALTER TABLE notify_log ADD COLUMN on_created INTEGER'
|
'ALTER TABLE notify_log ADD COLUMN on_created INTEGER'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade notify_log table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT poster_url FROM notify_log')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table notify_log.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE notify_log ADD COLUMN poster_url TEXT'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade notify_log table from earlier versions (populate table with data from notify_log)
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT timestamp FROM notify_log')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table notify_log.")
|
||||||
|
c_db.execute(
|
||||||
|
'CREATE TABLE IF NOT EXISTS notify_log_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, '
|
||||||
|
'session_key INTEGER, rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||||
|
'user_id INTEGER, user TEXT, agent_id INTEGER, agent_name TEXT, notify_action TEXT, '
|
||||||
|
'subject_text TEXT, body_text TEXT, script_args TEXT, poster_url TEXT)'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'INSERT INTO notify_log_temp (session_key, rating_key, user_id, user, agent_id, agent_name, '
|
||||||
|
'poster_url, timestamp, notify_action) '
|
||||||
|
'SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, timestamp, '
|
||||||
|
'notify_action FROM notify_log_temp '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_play, "play" FROM notify_log WHERE on_play '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_stop, "stop" FROM notify_log WHERE on_stop '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_watched, "watched" FROM notify_log WHERE on_watched '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_pause, "pause" FROM notify_log WHERE on_pause '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_resume, "resume" FROM notify_log WHERE on_resume '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_buffer, "buffer" FROM notify_log WHERE on_buffer '
|
||||||
|
'UNION ALL SELECT session_key, rating_key, user_id, user, agent_id, agent_name, poster_url, '
|
||||||
|
'on_created, "created" FROM notify_log WHERE on_created '
|
||||||
|
'ORDER BY timestamp ')
|
||||||
|
c_db.execute(
|
||||||
|
'DROP TABLE notify_log'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE notify_log_temp RENAME TO notify_log'
|
||||||
|
)
|
||||||
|
|
||||||
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
|
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
|
||||||
try:
|
try:
|
||||||
result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_library_sections_1")')
|
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()
|
||||||
if result and 'server_id' not in [row[2] for row in result]:
|
if 'section_id INTEGER UNIQUE' in result[0]:
|
||||||
logger.debug(u"Altering database. Removing unique constraint on section_id from library_sections table.")
|
logger.debug(u"Altering database. Removing unique constraint on section_id from library_sections table.")
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE library_sections_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE library_sections_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
@@ -739,7 +799,7 @@ def dbcheck():
|
|||||||
'ALTER TABLE library_sections_temp RENAME TO library_sections'
|
'ALTER TABLE library_sections_temp RENAME TO library_sections'
|
||||||
)
|
)
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
logger.debug(u"Unable to remove section_id unique constraint from library_sections.")
|
logger.warn(u"Unable to remove section_id unique constraint from library_sections.")
|
||||||
try:
|
try:
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'DROP TABLE library_sections_temp'
|
'DROP TABLE library_sections_temp'
|
||||||
@@ -747,10 +807,21 @@ def dbcheck():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Upgrade library_sections table from earlier versions (remove duplicated libraries)
|
||||||
|
try:
|
||||||
|
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""')
|
||||||
|
if result.rowcount > 0:
|
||||||
|
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
|
||||||
|
c_db.execute(
|
||||||
|
'DELETE FROM library_sections WHERE server_id = ""'
|
||||||
|
)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.warn(u"Unable to remove duplicate libraries from library_sections table.")
|
||||||
|
|
||||||
# Upgrade users table from earlier versions (remove UNIQUE constraint on username)
|
# Upgrade users table from earlier versions (remove UNIQUE constraint on username)
|
||||||
try:
|
try:
|
||||||
result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_users_2")')
|
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="users"').fetchone()
|
||||||
if result and 'username' in [row[2] for row in result]:
|
if 'username TEXT NOT NULL UNIQUE' in result[0]:
|
||||||
logger.debug(u"Altering database. Removing unique constraint on username from users table.")
|
logger.debug(u"Altering database. Removing unique constraint on username from users table.")
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE users_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE users_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
@@ -773,7 +844,7 @@ def dbcheck():
|
|||||||
'ALTER TABLE users_temp RENAME TO users'
|
'ALTER TABLE users_temp RENAME TO users'
|
||||||
)
|
)
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
logger.debug(u"Unable to remove username unique constraint from users.")
|
logger.warn(u"Unable to remove username unique constraint from users.")
|
||||||
try:
|
try:
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'DROP TABLE users_temp'
|
'DROP TABLE users_temp'
|
||||||
@@ -790,6 +861,7 @@ def dbcheck():
|
|||||||
conn_db.commit()
|
conn_db.commit()
|
||||||
c_db.close()
|
c_db.close()
|
||||||
|
|
||||||
|
|
||||||
def shutdown(restart=False, update=False):
|
def shutdown(restart=False, update=False):
|
||||||
cherrypy.engine.exit()
|
cherrypy.engine.exit()
|
||||||
SCHED.shutdown(wait=False)
|
SCHED.shutdown(wait=False)
|
||||||
@@ -822,6 +894,7 @@ def shutdown(restart=False, update=False):
|
|||||||
|
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
def generate_uuid():
|
def generate_uuid():
|
||||||
logger.debug(u"Generating UUID...")
|
logger.debug(u"Generating UUID...")
|
||||||
return uuid.uuid4().hex
|
return uuid.uuid4().hex
|
||||||
|
@@ -156,8 +156,8 @@ class ActivityHandler(object):
|
|||||||
(self.get_session_key(), buffer_last_triggered))
|
(self.get_session_key(), buffer_last_triggered))
|
||||||
time_since_last_trigger = int(time.time()) - int(buffer_last_triggered)
|
time_since_last_trigger = int(time.time()) - int(buffer_last_triggered)
|
||||||
|
|
||||||
if current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger == 0 or \
|
if plexpy.CONFIG.BUFFER_THRESHOLD > 0 and (current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and \
|
||||||
time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT:
|
time_since_last_trigger == 0 or time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT):
|
||||||
ap.set_session_buffer_trigger_time(session_key=self.get_session_key())
|
ap.set_session_buffer_trigger_time(session_key=self.get_session_key())
|
||||||
threading.Thread(target=notification_handler.notify,
|
threading.Thread(target=notification_handler.notify,
|
||||||
kwargs=dict(stream_data=db_stream, notify_action='buffer')).start()
|
kwargs=dict(stream_data=db_stream, notify_action='buffer')).start()
|
||||||
|
@@ -182,7 +182,7 @@ class ActivityProcessor(object):
|
|||||||
self.db.action(query=query, args=args)
|
self.db.action(query=query, args=args)
|
||||||
|
|
||||||
# Check if we should group the session, select the last two rows from the user
|
# Check if we should group the session, select the last two rows from the user
|
||||||
query = 'SELECT id, rating_key, user_id, reference_id FROM session_history \
|
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history \
|
||||||
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
|
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
|
||||||
|
|
||||||
args = [session['user_id']]
|
args = [session['user_id']]
|
||||||
@@ -191,6 +191,7 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
new_session = {'id': result[0]['id'],
|
new_session = {'id': result[0]['id'],
|
||||||
'rating_key': result[0]['rating_key'],
|
'rating_key': result[0]['rating_key'],
|
||||||
|
'view_offset': result[0]['view_offset'],
|
||||||
'user_id': result[0]['user_id'],
|
'user_id': result[0]['user_id'],
|
||||||
'reference_id': result[0]['reference_id']}
|
'reference_id': result[0]['reference_id']}
|
||||||
|
|
||||||
@@ -199,12 +200,14 @@ class ActivityProcessor(object):
|
|||||||
else:
|
else:
|
||||||
prev_session = {'id': result[1]['id'],
|
prev_session = {'id': result[1]['id'],
|
||||||
'rating_key': result[1]['rating_key'],
|
'rating_key': result[1]['rating_key'],
|
||||||
|
'view_offset': result[1]['view_offset'],
|
||||||
'user_id': result[1]['user_id'],
|
'user_id': result[1]['user_id'],
|
||||||
'reference_id': result[1]['reference_id']}
|
'reference_id': result[1]['reference_id']}
|
||||||
|
|
||||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
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 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']):
|
if (prev_session is not None) and (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']]
|
args = [prev_session['reference_id'], new_session['id']]
|
||||||
else:
|
else:
|
||||||
args = [new_session['id'], new_session['id']]
|
args = [new_session['id'], new_session['id']]
|
||||||
|
491
plexpy/api2.py
Normal file
491
plexpy/api2.py
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PlexPy is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
|
import database
|
||||||
|
import logger
|
||||||
|
import plexpy
|
||||||
|
|
||||||
|
|
||||||
|
class API2:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._api_valid_methods = self._api_docs().keys()
|
||||||
|
self._api_authenticated = False
|
||||||
|
self._api_out_type = 'json' # default
|
||||||
|
self._api_msg = None
|
||||||
|
self._api_debug = None
|
||||||
|
self._api_cmd = None
|
||||||
|
self._api_apikey = None
|
||||||
|
self._api_callback = None # JSONP
|
||||||
|
self._api_result_type = 'failed'
|
||||||
|
self._api_profileme = None # For profiling the api call
|
||||||
|
self._api_kwargs = None # Cleaned kwargs
|
||||||
|
|
||||||
|
def _api_docs(self, md=False):
|
||||||
|
""" Makes the api docs """
|
||||||
|
|
||||||
|
docs = {}
|
||||||
|
for f, _ in inspect.getmembers(self, predicate=inspect.ismethod):
|
||||||
|
if not f.startswith('_') and not f.startswith('_api'):
|
||||||
|
if md is True:
|
||||||
|
docs[f] = inspect.getdoc(getattr(self, f)) if inspect.getdoc(getattr(self, f)) else None
|
||||||
|
else:
|
||||||
|
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(getattr(self, f)) else None
|
||||||
|
return docs
|
||||||
|
|
||||||
|
def docs_md(self):
|
||||||
|
""" Return a API.md to simplify api docs because of the decorator. """
|
||||||
|
|
||||||
|
return self._api_make_md()
|
||||||
|
|
||||||
|
def docs(self):
|
||||||
|
""" Returns a dict where commands are keys, docstring are value. """
|
||||||
|
|
||||||
|
return self._api_docs()
|
||||||
|
|
||||||
|
def _api_validate(self, *args, **kwargs):
|
||||||
|
""" sets class vars and remove unneeded parameters. """
|
||||||
|
|
||||||
|
if not plexpy.CONFIG.API_ENABLED:
|
||||||
|
self._api_msg = 'API not enabled'
|
||||||
|
|
||||||
|
elif not plexpy.CONFIG.API_KEY:
|
||||||
|
self._api_msg = 'API key not generated'
|
||||||
|
|
||||||
|
elif len(plexpy.CONFIG.API_KEY) != 32:
|
||||||
|
self._api_msg = 'API key not generated correctly'
|
||||||
|
|
||||||
|
elif 'apikey' not in kwargs:
|
||||||
|
self._api_msg = 'Parameter apikey is required'
|
||||||
|
|
||||||
|
elif kwargs.get('apikey', '') != plexpy.CONFIG.API_KEY:
|
||||||
|
self._api_msg = 'Invalid apikey'
|
||||||
|
|
||||||
|
elif 'cmd' not in kwargs:
|
||||||
|
self._api_msg = 'Parameter cmd is required. Possible commands are: %s' % ', '.join(self._api_valid_methods)
|
||||||
|
|
||||||
|
elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods:
|
||||||
|
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(self._api_valid_methods))
|
||||||
|
|
||||||
|
self._api_callback = kwargs.pop('callback', None)
|
||||||
|
self._api_apikey = kwargs.pop('apikey', None)
|
||||||
|
self._api_cmd = kwargs.pop('cmd', None)
|
||||||
|
self._api_debug = kwargs.pop('debug', False)
|
||||||
|
self._api_profileme = kwargs.pop('profileme', None)
|
||||||
|
# Allow override for the api.
|
||||||
|
self._api_out_type = kwargs.pop('out_type', 'json')
|
||||||
|
|
||||||
|
if self._api_apikey == plexpy.CONFIG.API_KEY and plexpy.CONFIG.API_ENABLED and self._api_cmd in self._api_valid_methods:
|
||||||
|
self._api_authenticated = True
|
||||||
|
self._api_msg = None
|
||||||
|
self._api_kwargs = kwargs
|
||||||
|
elif self._api_cmd in ('get_apikey', 'docs', 'docs_md') and plexpy.CONFIG.API_ENABLED:
|
||||||
|
self._api_authenticated = True
|
||||||
|
# Remove the old error msg
|
||||||
|
self._api_msg = None
|
||||||
|
self._api_kwargs = kwargs
|
||||||
|
|
||||||
|
logger.debug(u'PlexPy APIv2 :: Cleaned kwargs %s' % self._api_kwargs)
|
||||||
|
|
||||||
|
return self._api_kwargs
|
||||||
|
|
||||||
|
def get_logs(self, sort='', search='', order='desc', regex='', start=0, end=0, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the log
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort(string, optional): time, thread, msg, loglevel
|
||||||
|
search(string, optional): 'string'
|
||||||
|
order(string, optional): desc, asc
|
||||||
|
regex(string, optional): 'regexstring'
|
||||||
|
start(int, optional): int
|
||||||
|
end(int, optional): int
|
||||||
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
```{"response":
|
||||||
|
{"msg": "Hey",
|
||||||
|
"result": "success"},
|
||||||
|
"data": [
|
||||||
|
{"time": "29-sept.2015",
|
||||||
|
"thread: "MainThread",
|
||||||
|
"msg: "Called x from y",
|
||||||
|
"loglevel": "DEBUG"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, 'plexpy.log')
|
||||||
|
templog = []
|
||||||
|
start = int(kwargs.get('start', 0))
|
||||||
|
end = int(kwargs.get('end', 0))
|
||||||
|
|
||||||
|
if regex:
|
||||||
|
logger.debug(u'PlexPy APIv2 :: Filtering log using regex %s' % regex)
|
||||||
|
reg = re.compile('u' + regex, flags=re.I)
|
||||||
|
|
||||||
|
for line in open(logfile, 'r').readlines():
|
||||||
|
temp_loglevel_and_time = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
temp_loglevel_and_time = line.split('- ')
|
||||||
|
loglvl = temp_loglevel_and_time[1].split(' :')[0].strip()
|
||||||
|
tl_tread = line.split(' :: ')
|
||||||
|
if loglvl is None:
|
||||||
|
msg = line.replace('\n', '')
|
||||||
|
else:
|
||||||
|
msg = line.split(' : ')[1].replace('\n', '')
|
||||||
|
thread = tl_tread[1].split(' : ')[0]
|
||||||
|
except IndexError:
|
||||||
|
# We assume this is a traceback
|
||||||
|
tl = (len(templog) - 1)
|
||||||
|
templog[tl]['msg'] += line.replace('\n', '')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
||||||
|
|
||||||
|
d = {
|
||||||
|
'time': temp_loglevel_and_time[0],
|
||||||
|
'loglevel': loglvl,
|
||||||
|
'msg': msg.replace('\n', ''),
|
||||||
|
'thread': thread
|
||||||
|
}
|
||||||
|
templog.append(d)
|
||||||
|
|
||||||
|
if end > 0 or start > 0:
|
||||||
|
logger.debug(u'PlexPy APIv2 :: Slicing the log from %s to %s' % (start, end))
|
||||||
|
templog = templog[start:end]
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
logger.debug(u'PlexPy APIv2 :: Sorting log based on %s' % sort)
|
||||||
|
templog = sorted(templog, key=lambda k: k[sort])
|
||||||
|
|
||||||
|
if search:
|
||||||
|
logger.debug(u'PlexPy APIv2 :: Searching log values for %s' % search)
|
||||||
|
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
|
||||||
|
|
||||||
|
if len(tt):
|
||||||
|
templog = tt
|
||||||
|
|
||||||
|
if regex:
|
||||||
|
tt = []
|
||||||
|
for l in templog:
|
||||||
|
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
|
||||||
|
if reg.search(stringdict):
|
||||||
|
tt.append(l)
|
||||||
|
|
||||||
|
if len(tt):
|
||||||
|
templog = tt
|
||||||
|
|
||||||
|
if order == 'desc':
|
||||||
|
templog = templog[::-1]
|
||||||
|
|
||||||
|
self.data = templog
|
||||||
|
return templog
|
||||||
|
|
||||||
|
def get_settings(self, key=''):
|
||||||
|
""" Fetches all settings from the config file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(string, optional): 'Run the it without args to see all args'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json:
|
||||||
|
```
|
||||||
|
{General: {api_enabled: true, ...}
|
||||||
|
Advanced: {cache_sizemb: "32", ...}}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
||||||
|
interface_list = [name for name in os.listdir(interface_dir) if
|
||||||
|
os.path.isdir(os.path.join(interface_dir, name))]
|
||||||
|
|
||||||
|
conf = plexpy.CONFIG._config
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Truthify the dict
|
||||||
|
for k, v in conf.iteritems():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
d = {}
|
||||||
|
for kk, vv in v.iteritems():
|
||||||
|
if vv == '0' or vv == '1':
|
||||||
|
d[kk] = bool(vv)
|
||||||
|
else:
|
||||||
|
d[kk] = vv
|
||||||
|
config[k] = d
|
||||||
|
if k == 'General':
|
||||||
|
config[k]['interface'] = interface_dir
|
||||||
|
config[k]['interface_list'] = interface_list
|
||||||
|
|
||||||
|
if key:
|
||||||
|
return config.get(key, None)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def sql(self, query=''):
|
||||||
|
""" Query the db with raw sql, makes backup of
|
||||||
|
the db if the backup is older then 24h
|
||||||
|
"""
|
||||||
|
if not plexpy.CONFIG.API_SQL or not query:
|
||||||
|
return
|
||||||
|
|
||||||
|
# allow the user to shoot them self
|
||||||
|
# in the foot but not in the head..
|
||||||
|
if not len(os.listdir(plexpy.BACKUP_DIR)):
|
||||||
|
self.backupdb()
|
||||||
|
else:
|
||||||
|
# If the backup is less then 24 h old lets make a backup
|
||||||
|
if any([os.path.getctime(os.path.join(plexpy.BACKUP_DIR, file_)) <
|
||||||
|
(time.time() - 86400) for file_ in os.listdir(plexpy.BACKUP_DIR)]):
|
||||||
|
self.backupdb()
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
rows = db.select(query)
|
||||||
|
self.data = rows
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def backupdb(self):
|
||||||
|
""" Creates a manual backup of the plexpy.db file """
|
||||||
|
|
||||||
|
data = database.make_backup()
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self.result_type = 'success'
|
||||||
|
else:
|
||||||
|
self.result_type = 'failed'
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def restart(self, **kwargs):
|
||||||
|
""" Restarts plexpy """
|
||||||
|
|
||||||
|
plexpy.SIGNAL = 'restart'
|
||||||
|
self.msg = 'Restarting plexpy'
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
""" Check for updates on Github """
|
||||||
|
|
||||||
|
plexpy.SIGNAL = 'update'
|
||||||
|
self.msg = 'Updating plexpy'
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
|
def _api_make_md(self):
|
||||||
|
""" Tries to make a API.md to simplify the api docs """
|
||||||
|
|
||||||
|
head = '''# API Reference\n
|
||||||
|
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
|
||||||
|
|
||||||
|
## General structure
|
||||||
|
The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command`
|
||||||
|
|
||||||
|
Response example
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"loglevel": "INFO",
|
||||||
|
"msg": "Signal 2 caught, saving and exiting...",
|
||||||
|
"thread": "MainThread",
|
||||||
|
"time": "22-sep-2015 01:42:56 "
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": null,
|
||||||
|
"result": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
General parameters:
|
||||||
|
out_type: 'xml',
|
||||||
|
callback: 'pong',
|
||||||
|
'debug': 1
|
||||||
|
|
||||||
|
## API methods'''
|
||||||
|
|
||||||
|
body = ''
|
||||||
|
doc = self._api_docs(md=True)
|
||||||
|
for k in sorted(doc):
|
||||||
|
v = doc.get(k)
|
||||||
|
body += '### %s\n' % k
|
||||||
|
body += '' if not v else v + '\n'
|
||||||
|
body += '\n\n'
|
||||||
|
|
||||||
|
result = head + '\n\n' + body
|
||||||
|
return '<div style="white-space: pre-wrap">' + result + '</div>'
|
||||||
|
|
||||||
|
def get_apikey(self, username='', password=''):
|
||||||
|
""" Fetches apikey
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username(string, optional): Your username
|
||||||
|
password(string, optional): Your password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: Apikey, args are required if auth is enabled
|
||||||
|
makes and saves the apikey it does not exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
apikey = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32]
|
||||||
|
if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
|
if username == plexpy.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
|
if plexpy.CONFIG.API_KEY:
|
||||||
|
self.data = plexpy.CONFIG.API_KEY
|
||||||
|
else:
|
||||||
|
self.data = apikey
|
||||||
|
plexpy.CONFIG.API_KEY = apikey
|
||||||
|
plexpy.CONFIG.write()
|
||||||
|
else:
|
||||||
|
self.msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
||||||
|
else:
|
||||||
|
if plexpy.CONFIG.API_KEY:
|
||||||
|
self.data = plexpy.CONFIG.API_KEY
|
||||||
|
else:
|
||||||
|
# Make a apikey if the doesn't exist
|
||||||
|
self.data = apikey
|
||||||
|
plexpy.CONFIG.API_KEY = apikey
|
||||||
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
def _api_responds(self, result_type='success', data=None, msg=''):
|
||||||
|
""" Formats the result to a predefined dict so we can hange it the to
|
||||||
|
the desired output by _api_out_as """
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
|
return {"response": {"result": result_type, "message": msg, "data": data}}
|
||||||
|
|
||||||
|
def _api_out_as(self, out):
|
||||||
|
""" Formats the response to the desired output """
|
||||||
|
|
||||||
|
if self._api_cmd == 'docs_md':
|
||||||
|
return out['response']['data']
|
||||||
|
|
||||||
|
if self._api_out_type == 'json':
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||||
|
try:
|
||||||
|
if self._api_debug:
|
||||||
|
out = json.dumps(out, indent=4, sort_keys=True)
|
||||||
|
else:
|
||||||
|
out = json.dumps(out)
|
||||||
|
if self._api_callback is not None:
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/javascript'
|
||||||
|
# wrap with JSONP call if requested
|
||||||
|
out = self._api_callback + '(' + out + ');'
|
||||||
|
# if we fail to generate the output fake an error
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(u'PlexPy APIv2 :: ' + traceback.format_exc())
|
||||||
|
out['message'] = traceback.format_exc()
|
||||||
|
out['result'] = 'error'
|
||||||
|
elif self._api_out_type == 'xml':
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/xml'
|
||||||
|
try:
|
||||||
|
out = xmltodict.unparse(out, pretty=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(u'PlexPy APIv2 :: Failed to parse xml result')
|
||||||
|
try:
|
||||||
|
out['message'] = e
|
||||||
|
out['result'] = 'error'
|
||||||
|
out = xmltodict.unparse(out, pretty=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(u'PlexPy APIv2 :: Failed to parse xml result error message %s' % e)
|
||||||
|
out = '''<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response>
|
||||||
|
<message>%s</message>
|
||||||
|
<data></data>
|
||||||
|
<result>error</result>
|
||||||
|
</response>
|
||||||
|
''' % e
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _api_run(self, *args, **kwargs):
|
||||||
|
""" handles the stuff from the handler """
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
logger.debug(u'PlexPy APIv2 :: Original kwargs was %s' % kwargs)
|
||||||
|
|
||||||
|
self._api_validate(**kwargs)
|
||||||
|
|
||||||
|
if self._api_cmd and self._api_authenticated:
|
||||||
|
call = getattr(self, self._api_cmd)
|
||||||
|
|
||||||
|
# Profile is written to console.
|
||||||
|
if self._api_profileme:
|
||||||
|
from profilehooks import profile
|
||||||
|
call = profile(call, immediate=True)
|
||||||
|
|
||||||
|
# We allow this to fail so we get a
|
||||||
|
# traceback in the browser
|
||||||
|
if self._api_debug:
|
||||||
|
result = call(**self._api_kwargs)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
result = call(**self._api_kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(u'PlexPy APIv2 :: Failed to run %s %s %s' % (self._api_cmd, self._api_kwargs, e))
|
||||||
|
|
||||||
|
ret = None
|
||||||
|
# The api decorated function can return different result types.
|
||||||
|
# convert it to a list/dict before we change it to the users
|
||||||
|
# wanted output
|
||||||
|
try:
|
||||||
|
if isinstance(result, (dict, list)):
|
||||||
|
ret = result
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
ret = json.loads(result)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
try:
|
||||||
|
ret = xmltodict.parse(result, attr_prefix='')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback if we cant "parse the reponse"
|
||||||
|
if ret is None:
|
||||||
|
ret = result
|
||||||
|
|
||||||
|
if ret or self._api_result_type == 'success':
|
||||||
|
# To allow override for restart etc
|
||||||
|
# if the call returns some data we are gonna assume its a success
|
||||||
|
self._api_result_type = 'success'
|
||||||
|
else:
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
|
||||||
|
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
@@ -57,4 +57,15 @@ MEDIA_FLAGS_AUDIO = {'ac.?3': 'dolby_digital',
|
|||||||
MEDIA_FLAGS_VIDEO = {'avc1': 'h264',
|
MEDIA_FLAGS_VIDEO = {'avc1': 'h264',
|
||||||
'wmv(1|2)': 'wmv',
|
'wmv(1|2)': 'wmv',
|
||||||
'wmv3': 'wmvhd'
|
'wmv3': 'wmvhd'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCHEDULER_LIST = ['Check GitHub for updates',
|
||||||
|
'Check for active sessions',
|
||||||
|
'Check for recently added items',
|
||||||
|
'Check for Plex remote access',
|
||||||
|
'Refresh users list',
|
||||||
|
'Refresh libraries list',
|
||||||
|
'Refresh Plex server URLs',
|
||||||
|
'Refresh Plex server name',
|
||||||
|
'Backup PlexPy database'
|
||||||
|
]
|
@@ -25,13 +25,15 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PMS_NAME': (unicode, 'PMS', ''),
|
'PMS_NAME': (unicode, 'PMS', ''),
|
||||||
'PMS_PORT': (int, 'PMS', 32400),
|
'PMS_PORT': (int, 'PMS', 32400),
|
||||||
'PMS_TOKEN': (str, 'PMS', ''),
|
'PMS_TOKEN': (str, 'PMS', ''),
|
||||||
'PMS_SSL': (int, 'General', 0),
|
'PMS_SSL': (int, 'PMS', 0),
|
||||||
'PMS_URL': (str, 'PMS', ''),
|
'PMS_URL': (str, 'PMS', ''),
|
||||||
'PMS_USE_BIF': (int, 'PMS', 0),
|
'PMS_USE_BIF': (int, 'PMS', 0),
|
||||||
'PMS_UUID': (str, 'PMS', ''),
|
'PMS_UUID': (str, 'PMS', ''),
|
||||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||||
|
'ANON_REDIRECT': (str, 'General', 'http://dereferer.org/?'),
|
||||||
'API_ENABLED': (int, 'General', 0),
|
'API_ENABLED': (int, 'General', 0),
|
||||||
'API_KEY': (str, 'General', ''),
|
'API_KEY': (str, 'General', ''),
|
||||||
|
'API_SQL': (int, 'General', 0),
|
||||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
||||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
||||||
'BOXCAR_SOUND': (str, 'Boxcar', ''),
|
'BOXCAR_SOUND': (str, 'Boxcar', ''),
|
||||||
@@ -48,6 +50,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'BOXCAR_ON_INTUP': (int, 'Boxcar', 0),
|
'BOXCAR_ON_INTUP': (int, 'Boxcar', 0),
|
||||||
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
|
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
|
||||||
'BUFFER_WAIT': (int, 'Monitoring', 900),
|
'BUFFER_WAIT': (int, 'Monitoring', 900),
|
||||||
|
'BACKUP_DIR': (str, 'General', ''),
|
||||||
'CACHE_DIR': (str, 'General', ''),
|
'CACHE_DIR': (str, 'General', ''),
|
||||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||||
'CHECK_GITHUB': (int, 'General', 1),
|
'CHECK_GITHUB': (int, 'General', 1),
|
||||||
@@ -85,6 +88,8 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'FACEBOOK_APP_SECRET': (str, 'Facebook', ''),
|
'FACEBOOK_APP_SECRET': (str, 'Facebook', ''),
|
||||||
'FACEBOOK_TOKEN': (str, 'Facebook', ''),
|
'FACEBOOK_TOKEN': (str, 'Facebook', ''),
|
||||||
'FACEBOOK_GROUP': (str, 'Facebook', ''),
|
'FACEBOOK_GROUP': (str, 'Facebook', ''),
|
||||||
|
'FACEBOOK_INCL_POSTER': (int, 'Facebook', 1),
|
||||||
|
'FACEBOOK_INCL_SUBJECT': (int, 'Facebook', 1),
|
||||||
'FACEBOOK_ON_PLAY': (int, 'Facebook', 0),
|
'FACEBOOK_ON_PLAY': (int, 'Facebook', 0),
|
||||||
'FACEBOOK_ON_STOP': (int, 'Facebook', 0),
|
'FACEBOOK_ON_STOP': (int, 'Facebook', 0),
|
||||||
'FACEBOOK_ON_PAUSE': (int, 'Facebook', 0),
|
'FACEBOOK_ON_PAUSE': (int, 'Facebook', 0),
|
||||||
@@ -127,8 +132,11 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'HOME_STATS_COUNT': (int, 'General', 5),
|
'HOME_STATS_COUNT': (int, 'General', 5),
|
||||||
'HOME_STATS_CARDS': (list, 'General', ['top_tv', 'popular_tv', 'top_movies', 'popular_movies', 'top_music', \
|
'HOME_STATS_CARDS': (list, 'General', ['top_tv', 'popular_tv', 'top_movies', 'popular_movies', 'top_music', \
|
||||||
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
|
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
|
||||||
|
'HTTPS_CREATE_CERT': (int, 'General', 1),
|
||||||
'HTTPS_CERT': (str, 'General', ''),
|
'HTTPS_CERT': (str, 'General', ''),
|
||||||
'HTTPS_KEY': (str, 'General', ''),
|
'HTTPS_KEY': (str, 'General', ''),
|
||||||
|
'HTTPS_DOMAIN': (str, 'General', 'localhost'),
|
||||||
|
'HTTPS_IP': (str, 'General', '127.0.0.1'),
|
||||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||||
'HTTP_PASSWORD': (str, 'General', ''),
|
'HTTP_PASSWORD': (str, 'General', ''),
|
||||||
'HTTP_PORT': (int, 'General', 8181),
|
'HTTP_PORT': (int, 'General', 8181),
|
||||||
@@ -183,6 +191,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'NMA_ON_EXTUP': (int, 'NMA', 0),
|
'NMA_ON_EXTUP': (int, 'NMA', 0),
|
||||||
'NMA_ON_INTUP': (int, 'NMA', 0),
|
'NMA_ON_INTUP': (int, 'NMA', 0),
|
||||||
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
|
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
|
||||||
|
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
||||||
@@ -282,6 +291,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PUSHBULLET_ON_INTUP': (int, 'PushBullet', 0),
|
'PUSHBULLET_ON_INTUP': (int, 'PushBullet', 0),
|
||||||
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
|
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
|
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_HTML_SUPPORT': (int, 'Pushover', 1),
|
||||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||||
'PUSHOVER_SOUND': (str, 'Pushover', ''),
|
'PUSHOVER_SOUND': (str, 'Pushover', ''),
|
||||||
@@ -304,6 +314,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'SLACK_HOOK': (str, 'Slack', ''),
|
'SLACK_HOOK': (str, 'Slack', ''),
|
||||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||||
'SLACK_ICON_EMOJI': (str, 'Slack', ''),
|
'SLACK_ICON_EMOJI': (str, 'Slack', ''),
|
||||||
|
'SLACK_INCL_SUBJECT': (int, 'Slack', 1),
|
||||||
'SLACK_USERNAME': (str, 'Slack', ''),
|
'SLACK_USERNAME': (str, 'Slack', ''),
|
||||||
'SLACK_ON_PLAY': (int, 'Slack', 0),
|
'SLACK_ON_PLAY': (int, 'Slack', 0),
|
||||||
'SLACK_ON_STOP': (int, 'Slack', 0),
|
'SLACK_ON_STOP': (int, 'Slack', 0),
|
||||||
@@ -343,6 +354,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
||||||
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
||||||
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
|
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
|
||||||
|
'TELEGRAM_INCL_SUBJECT': (int, 'Telegram', 1),
|
||||||
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
|
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
|
||||||
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
|
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
|
||||||
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
|
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
|
||||||
@@ -360,9 +372,11 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
||||||
'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
||||||
'TWITTER_ENABLED': (int, 'Twitter', 0),
|
'TWITTER_ENABLED': (int, 'Twitter', 0),
|
||||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
'TWITTER_ACCESS_TOKEN': (str, 'Twitter', ''),
|
||||||
'TWITTER_PREFIX': (str, 'Twitter', 'PlexPy'),
|
'TWITTER_ACCESS_TOKEN_SECRET': (str, 'Twitter', ''),
|
||||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
'TWITTER_CONSUMER_KEY': (str, 'Twitter', ''),
|
||||||
|
'TWITTER_CONSUMER_SECRET': (str, 'Twitter', ''),
|
||||||
|
'TWITTER_INCL_SUBJECT': (int, 'Twitter', 1),
|
||||||
'TWITTER_ON_PLAY': (int, 'Twitter', 0),
|
'TWITTER_ON_PLAY': (int, 'Twitter', 0),
|
||||||
'TWITTER_ON_STOP': (int, 'Twitter', 0),
|
'TWITTER_ON_STOP': (int, 'Twitter', 0),
|
||||||
'TWITTER_ON_PAUSE': (int, 'Twitter', 0),
|
'TWITTER_ON_PAUSE': (int, 'Twitter', 0),
|
||||||
@@ -511,6 +525,7 @@ class Config(object):
|
|||||||
self.MOVIE_LOGGING_ENABLE = 0
|
self.MOVIE_LOGGING_ENABLE = 0
|
||||||
self.TV_LOGGING_ENABLE = 0
|
self.TV_LOGGING_ENABLE = 0
|
||||||
self.CONFIG_VERSION = '1'
|
self.CONFIG_VERSION = '1'
|
||||||
|
|
||||||
if self.CONFIG_VERSION == '1':
|
if self.CONFIG_VERSION == '1':
|
||||||
# Change home_stats_cards to list
|
# Change home_stats_cards to list
|
||||||
if self.HOME_STATS_CARDS:
|
if self.HOME_STATS_CARDS:
|
||||||
@@ -524,4 +539,20 @@ class Config(object):
|
|||||||
if 'library_statistics' in home_library_cards:
|
if 'library_statistics' in home_library_cards:
|
||||||
home_library_cards.remove('library_statistics')
|
home_library_cards.remove('library_statistics')
|
||||||
self.HOME_LIBRARY_CARDS = home_library_cards
|
self.HOME_LIBRARY_CARDS = home_library_cards
|
||||||
self.CONFIG_VERSION = '2'
|
self.CONFIG_VERSION = '2'
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == '2':
|
||||||
|
self.NOTIFY_ON_START_SUBJECT_TEXT = self.NOTIFY_ON_START_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_START_BODY_TEXT = self.NOTIFY_ON_START_BODY_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_STOP_SUBJECT_TEXT = self.NOTIFY_ON_STOP_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_STOP_BODY_TEXT = self.NOTIFY_ON_STOP_BODY_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_PAUSE_SUBJECT_TEXT = self.NOTIFY_ON_PAUSE_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_PAUSE_BODY_TEXT = self.NOTIFY_ON_PAUSE_BODY_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_RESUME_SUBJECT_TEXT = self.NOTIFY_ON_RESUME_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_RESUME_BODY_TEXT = self.NOTIFY_ON_RESUME_BODY_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_BUFFER_SUBJECT_TEXT = self.NOTIFY_ON_BUFFER_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_BUFFER_BODY_TEXT = self.NOTIFY_ON_BUFFER_BODY_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_WATCHED_SUBJECT_TEXT = self.NOTIFY_ON_WATCHED_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_ON_WATCHED_BODY_TEXT = self.NOTIFY_ON_WATCHED_BODY_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.NOTIFY_SCRIPTS_ARGS_TEXT = self.NOTIFY_SCRIPTS_ARGS_TEXT.replace('{progress}','{progress_duration}')
|
||||||
|
self.CONFIG_VERSION = '3'
|
||||||
|
@@ -13,20 +13,24 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger
|
import arrow
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import os
|
import os
|
||||||
import plexpy
|
import sqlite3
|
||||||
import time
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import logger
|
||||||
|
import plexpy
|
||||||
|
|
||||||
|
|
||||||
db_lock = threading.Lock()
|
db_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def drop_session_db():
|
def drop_session_db():
|
||||||
monitor_db = MonitorDatabase()
|
monitor_db = MonitorDatabase()
|
||||||
monitor_db.action('DROP TABLE sessions')
|
monitor_db.action('DROP TABLE sessions')
|
||||||
|
|
||||||
|
|
||||||
def clear_history_tables():
|
def clear_history_tables():
|
||||||
logger.debug(u"PlexPy Database :: Deleting all session_history records... No turning back now bub.")
|
logger.debug(u"PlexPy Database :: Deleting all session_history records... No turning back now bub.")
|
||||||
monitor_db = MonitorDatabase()
|
monitor_db = MonitorDatabase()
|
||||||
@@ -35,10 +39,52 @@ def clear_history_tables():
|
|||||||
monitor_db.action('DELETE FROM session_history_metadata')
|
monitor_db.action('DELETE FROM session_history_metadata')
|
||||||
monitor_db.action('VACUUM;')
|
monitor_db.action('VACUUM;')
|
||||||
|
|
||||||
|
|
||||||
def db_filename(filename="plexpy.db"):
|
def db_filename(filename="plexpy.db"):
|
||||||
|
""" Returns the filepath to the db """
|
||||||
|
|
||||||
return os.path.join(plexpy.DATA_DIR, filename)
|
return os.path.join(plexpy.DATA_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
|
def make_backup(cleanup=False, scheduler=False):
|
||||||
|
""" Makes a backup of db, removes all but the last 5 backups """
|
||||||
|
|
||||||
|
if scheduler:
|
||||||
|
backup_file = 'plexpy.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
|
||||||
|
else:
|
||||||
|
backup_file = 'plexpy.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
|
||||||
|
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
||||||
|
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||||
|
|
||||||
|
# In case the user has deleted it manually
|
||||||
|
if not os.path.exists(backup_folder):
|
||||||
|
os.makedirs(backup_folder)
|
||||||
|
|
||||||
|
db = MonitorDatabase()
|
||||||
|
db.connection.execute('begin immediate')
|
||||||
|
shutil.copyfile(db_filename(), backup_file_fp)
|
||||||
|
db.connection.rollback()
|
||||||
|
|
||||||
|
if cleanup:
|
||||||
|
# Delete all scheduled backup files except from the last 5.
|
||||||
|
for root, dirs, files in os.walk(backup_folder):
|
||||||
|
db_files = [os.path.join(root, f) for f in files if f.endswith('.sched.db')]
|
||||||
|
if len(db_files) > 5:
|
||||||
|
backups_sorted_on_age = sorted(db_files, key=os.path.getctime, reverse=True)
|
||||||
|
for file_ in backups_sorted_on_age[5:]:
|
||||||
|
try:
|
||||||
|
os.remove(file_)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(u"PlexPy Database :: Failed to delete %s from the backup folder: %s" % (file_, e))
|
||||||
|
|
||||||
|
if backup_file in os.listdir(backup_folder):
|
||||||
|
logger.debug(u"PlexPy Database :: Successfully backed up %s to %s" % (db_filename(), backup_file))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warn(u"PlexPy Database :: Failed to backup %s to %s" % (db_filename(), backup_file))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_cache_size():
|
def get_cache_size():
|
||||||
# This will protect against typecasting problems produced by empty string and None settings
|
# This will protect against typecasting problems produced by empty string and None settings
|
||||||
if not plexpy.CONFIG.CACHE_SIZEMB:
|
if not plexpy.CONFIG.CACHE_SIZEMB:
|
||||||
@@ -46,6 +92,7 @@ def get_cache_size():
|
|||||||
return 0
|
return 0
|
||||||
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
||||||
|
|
||||||
|
|
||||||
def dict_factory(cursor, row):
|
def dict_factory(cursor, row):
|
||||||
d = {}
|
d = {}
|
||||||
for idx, col in enumerate(cursor.description):
|
for idx, col in enumerate(cursor.description):
|
||||||
@@ -87,15 +134,15 @@ class MonitorDatabase(object):
|
|||||||
|
|
||||||
except sqlite3.OperationalError, e:
|
except sqlite3.OperationalError, e:
|
||||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||||
logger.warn('Database Error: %s', e)
|
logger.warn(u"PlexPy Database :: Database Error: %s", e)
|
||||||
attempts += 1
|
attempts += 1
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
logger.error('Database error: %s', e)
|
logger.error(u"PlexPy Database :: Database error: %s", e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except sqlite3.DatabaseError, e:
|
except sqlite3.DatabaseError, e:
|
||||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
logger.error(u"PlexPy Database :: Fatal Error executing %s :: %s", query, e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return sql_result
|
return sql_result
|
||||||
@@ -139,7 +186,7 @@ class MonitorDatabase(object):
|
|||||||
try:
|
try:
|
||||||
self.action(insert_query, value_dict.values() + key_dict.values())
|
self.action(insert_query, value_dict.values() + key_dict.values())
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
logger.info(u"PlexPy Database :: Queries failed: %s and %s", update_query, insert_query)
|
||||||
|
|
||||||
# We want to know if it was an update or insert
|
# We want to know if it was an update or insert
|
||||||
return trans_type
|
return trans_type
|
||||||
|
@@ -58,7 +58,7 @@ class DataFactory(object):
|
|||||||
'session_history_metadata.thumb',
|
'session_history_metadata.thumb',
|
||||||
'session_history_metadata.parent_thumb',
|
'session_history_metadata.parent_thumb',
|
||||||
'session_history_metadata.grandparent_thumb',
|
'session_history_metadata.grandparent_thumb',
|
||||||
'((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \
|
'MAX((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \
|
||||||
(CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 \
|
(CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 \
|
||||||
ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete',
|
ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete',
|
||||||
'session_history_media_info.video_decision',
|
'session_history_media_info.video_decision',
|
||||||
@@ -664,7 +664,8 @@ class DataFactory(object):
|
|||||||
for id in library_cards:
|
for id in library_cards:
|
||||||
if id.isdigit():
|
if id.isdigit():
|
||||||
try:
|
try:
|
||||||
query = 'SELECT section_id, section_name, section_type, thumb, count, parent_count, child_count ' \
|
query = 'SELECT section_id, section_name, section_type, thumb AS library_thumb, ' \
|
||||||
|
'custom_thumb_url AS custom_thumb, count, parent_count, child_count ' \
|
||||||
'FROM library_sections ' \
|
'FROM library_sections ' \
|
||||||
'WHERE section_id = %s ' % id
|
'WHERE section_id = %s ' % id
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
@@ -673,10 +674,17 @@ class DataFactory(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
|
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
||||||
|
library_thumb = item['custom_thumb']
|
||||||
|
elif item['library_thumb']:
|
||||||
|
library_thumb = item['library_thumb']
|
||||||
|
else:
|
||||||
|
library_thumb = common.DEFAULT_COVER_THUMB
|
||||||
|
|
||||||
library = {'section_id': item['section_id'],
|
library = {'section_id': item['section_id'],
|
||||||
'section_name': item['section_name'],
|
'section_name': item['section_name'],
|
||||||
'section_type': item['section_type'],
|
'section_type': item['section_type'],
|
||||||
'thumb': item['thumb'],
|
'thumb': library_thumb,
|
||||||
'count': item['count'],
|
'count': item['count'],
|
||||||
'parent_count': item['parent_count'],
|
'parent_count': item['parent_count'],
|
||||||
'child_count': item['child_count']
|
'child_count': item['child_count']
|
||||||
@@ -835,19 +843,45 @@ class DataFactory(object):
|
|||||||
def get_session_ip(self, session_key=''):
|
def get_session_ip(self, session_key=''):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if session_key:
|
|
||||||
query = 'SELECT ip_address FROM sessions WHERE session_key = %d' % int(session_key)
|
|
||||||
result = monitor_db.select(query)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ip_address = 'N/A'
|
ip_address = 'N/A'
|
||||||
|
|
||||||
|
if session_key:
|
||||||
|
try:
|
||||||
|
query = 'SELECT ip_address FROM sessions WHERE session_key = %d' % int(session_key)
|
||||||
|
result = monitor_db.select(query)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"PlexPy DataFactory :: Unable to execute database query for get_session_ip: %s." % e)
|
||||||
|
return ip_address
|
||||||
|
else:
|
||||||
|
return ip_address
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
ip_address = item['ip_address']
|
ip_address = item['ip_address']
|
||||||
|
|
||||||
return ip_address
|
return ip_address
|
||||||
|
|
||||||
|
def get_poster_url(self, rating_key=''):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
poster_url = ''
|
||||||
|
|
||||||
|
if rating_key:
|
||||||
|
try:
|
||||||
|
query = 'SELECT id, poster_url FROM notify_log ' \
|
||||||
|
'WHERE rating_key = %d OR parent_rating_key = %d OR grandparent_rating_key = %d ' \
|
||||||
|
'ORDER BY id DESC LIMIT 1' % (int(rating_key), int(rating_key), int(rating_key))
|
||||||
|
result = monitor_db.select(query)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"PlexPy DataFactory :: Unable to execute database query for get_poster_url: %s." % e)
|
||||||
|
return poster_url
|
||||||
|
else:
|
||||||
|
return poster_url
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
poster_url = item['poster_url']
|
||||||
|
|
||||||
|
return poster_url
|
||||||
|
|
||||||
def get_search_query(self, rating_key=''):
|
def get_search_query(self, rating_key=''):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
@@ -1086,4 +1120,72 @@ class DataFactory(object):
|
|||||||
metadata['studio'],
|
metadata['studio'],
|
||||||
old_rating_key]
|
old_rating_key]
|
||||||
|
|
||||||
monitor_db.action(query=query, args=args)
|
monitor_db.action(query=query, args=args)
|
||||||
|
|
||||||
|
def get_notification_log(self, kwargs=None):
|
||||||
|
data_tables = datatables.DataTables()
|
||||||
|
|
||||||
|
columns = ['notify_log.id',
|
||||||
|
'notify_log.timestamp',
|
||||||
|
'notify_log.session_key',
|
||||||
|
'notify_log.rating_key',
|
||||||
|
'notify_log.user_id',
|
||||||
|
'notify_log.user',
|
||||||
|
'notify_log.agent_id',
|
||||||
|
'notify_log.agent_name',
|
||||||
|
'notify_log.notify_action',
|
||||||
|
'notify_log.subject_text',
|
||||||
|
'notify_log.body_text',
|
||||||
|
'notify_log.script_args',
|
||||||
|
'notify_log.poster_url',
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
query = data_tables.ssp_query(table_name='notify_log',
|
||||||
|
columns=columns,
|
||||||
|
custom_where=[],
|
||||||
|
group_by=[],
|
||||||
|
join_types=[],
|
||||||
|
join_tables=[],
|
||||||
|
join_evals=[],
|
||||||
|
kwargs=kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"PlexPy DataFactory :: Unable to execute database query for get_notification_log: %s." % e)
|
||||||
|
return {'recordsFiltered': 0,
|
||||||
|
'recordsTotal': 0,
|
||||||
|
'draw': 0,
|
||||||
|
'data': 'null',
|
||||||
|
'error': 'Unable to execute database query.'}
|
||||||
|
|
||||||
|
notifications = query['result']
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for item in notifications:
|
||||||
|
if item['body_text']:
|
||||||
|
body_text = item['body_text'].replace('\r\n', '<br />').replace('\n', '<br />')
|
||||||
|
else:
|
||||||
|
body_text = ''
|
||||||
|
|
||||||
|
row = {'id': item['id'],
|
||||||
|
'timestamp': item['timestamp'],
|
||||||
|
'session_key': item['session_key'],
|
||||||
|
'rating_key': item['rating_key'],
|
||||||
|
'user_id': item['user_id'],
|
||||||
|
'user': item['user'],
|
||||||
|
'agent_id': item['agent_id'],
|
||||||
|
'agent_name': item['agent_name'],
|
||||||
|
'notify_action': item['notify_action'],
|
||||||
|
'subject_text': item['subject_text'],
|
||||||
|
'body_text': body_text,
|
||||||
|
'script_args': item['script_args'],
|
||||||
|
'poster_url': item['poster_url']
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
dict = {'recordsFiltered': query['filteredCount'],
|
||||||
|
'recordsTotal': query['totalCount'],
|
||||||
|
'data': rows,
|
||||||
|
'draw': query['draw']
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict
|
@@ -14,9 +14,9 @@
|
|||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger, database, helpers, common
|
from plexpy import logger, database, helpers, common
|
||||||
|
import plexpy
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import locale
|
|
||||||
|
|
||||||
|
|
||||||
class Graphs(object):
|
class Graphs(object):
|
||||||
@@ -321,7 +321,7 @@ class Graphs(object):
|
|||||||
dt = datetime.datetime(*month_item[:6])
|
dt = datetime.datetime(*month_item[:6])
|
||||||
date_string = dt.strftime('%Y-%m')
|
date_string = dt.strftime('%Y-%m')
|
||||||
|
|
||||||
categories.append(dt.strftime('%b %Y').decode(locale.getlocale()[1]))
|
categories.append(dt.strftime('%b %Y').decode(plexpy.SYS_ENCODING, 'replace'))
|
||||||
series_1_value = 0
|
series_1_value = 0
|
||||||
series_2_value = 0
|
series_2_value = 0
|
||||||
series_3_value = 0
|
series_3_value = 0
|
||||||
|
@@ -13,22 +13,61 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from operator import itemgetter
|
import base64
|
||||||
from xml.dom import minidom
|
|
||||||
|
|
||||||
import unicodedata
|
|
||||||
import plexpy
|
|
||||||
import datetime
|
import datetime
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import shutil
|
from functools import wraps
|
||||||
import time
|
from IPy import IP
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import xmltodict
|
|
||||||
import math
|
import math
|
||||||
|
from operator import itemgetter
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import unicodedata
|
||||||
|
import urllib, urllib2
|
||||||
|
from xml.dom import minidom
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
|
import plexpy
|
||||||
|
from api2 import API2
|
||||||
|
|
||||||
|
|
||||||
|
def addtoapi(*dargs, **dkwargs):
|
||||||
|
""" Helper decorator that adds function to the API class.
|
||||||
|
is used to reuse as much code as possible
|
||||||
|
|
||||||
|
args:
|
||||||
|
dargs: (string, optional) Used to rename a function
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@addtoapi("i_was_renamed", "im_a_second_alias")
|
||||||
|
@addtoapi()
|
||||||
|
|
||||||
|
"""
|
||||||
|
def rd(function):
|
||||||
|
@wraps(function)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
if dargs:
|
||||||
|
# To rename the function if it sucks.. and
|
||||||
|
# allow compat with old api.
|
||||||
|
for n in dargs:
|
||||||
|
if function.__doc__ and len(function.__doc__):
|
||||||
|
function.__doc__ = function.__doc__.strip()
|
||||||
|
setattr(API2, n, function)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
if function.__doc__ and len(function.__doc__):
|
||||||
|
function.__doc__ = function.__doc__.strip()
|
||||||
|
setattr(API2, function.__name__, function)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return rd
|
||||||
|
|
||||||
def multikeysort(items, columns):
|
def multikeysort(items, columns):
|
||||||
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
||||||
@@ -135,6 +174,15 @@ def convert_seconds(s):
|
|||||||
|
|
||||||
return minutes
|
return minutes
|
||||||
|
|
||||||
|
def convert_seconds_to_minutes(s):
|
||||||
|
|
||||||
|
if str(s).isdigit():
|
||||||
|
minutes = round(float(s) / 60, 0)
|
||||||
|
|
||||||
|
return math.trunc(minutes)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def today():
|
def today():
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
@@ -164,7 +212,7 @@ def human_duration(s, sig='dhms'):
|
|||||||
if sig >= 'dh' and h > 0:
|
if sig >= 'dh' and h > 0:
|
||||||
h = h + 1 if sig == 'dh' and m >= 30 else h
|
h = h + 1 if sig == 'dh' and m >= 30 else h
|
||||||
hd_list.append(str(h) + ' hrs')
|
hd_list.append(str(h) + ' hrs')
|
||||||
|
|
||||||
if sig >= 'dhm' and m > 0:
|
if sig >= 'dhm' and m > 0:
|
||||||
m = m + 1 if sig == 'dhm' and s >= 30 else m
|
m = m + 1 if sig == 'dhm' and s >= 30 else m
|
||||||
hd_list.append(str(m) + ' mins')
|
hd_list.append(str(m) + ' mins')
|
||||||
@@ -332,7 +380,7 @@ def split_string(mystring, splitvar=','):
|
|||||||
|
|
||||||
def create_https_certificates(ssl_cert, ssl_key):
|
def create_https_certificates(ssl_cert, ssl_key):
|
||||||
"""
|
"""
|
||||||
Create a pair of self-signed HTTPS certificares and store in them in
|
Create a self-signed HTTPS certificate and store in it in
|
||||||
'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed.
|
'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed.
|
||||||
|
|
||||||
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
|
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
|
||||||
@@ -341,24 +389,24 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||||||
from plexpy import logger
|
from plexpy import logger
|
||||||
|
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
from certgen import createKeyPair, createCertRequest, createCertificate, \
|
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
||||||
TYPE_RSA, serial
|
|
||||||
|
|
||||||
# Create the CA Certificate
|
serial = int(time.time())
|
||||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
||||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
|
||||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
altNames = ','.join(domains + ips)
|
||||||
|
|
||||||
|
# Create the self-signed PlexPy certificate
|
||||||
|
logger.debug(u"Generating self-signed SSL certificate.")
|
||||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||||
req = createCertRequest(pkey, CN="PlexPy")
|
cert = createSelfSignedCertificate(("PlexPy", pkey), serial, (0, 60 * 60 * 24 * 365 * 10), altNames) # ten years
|
||||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
|
||||||
|
|
||||||
# Save the key and certificate to disk
|
# Save the key and certificate to disk
|
||||||
try:
|
try:
|
||||||
with open(ssl_key, "w") as fp:
|
|
||||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
|
||||||
with open(ssl_cert, "w") as fp:
|
with open(ssl_cert, "w") as fp:
|
||||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||||
|
with open(ssl_key, "w") as fp:
|
||||||
|
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error("Error creating SSL key and certificate: %s", e)
|
logger.error("Error creating SSL key and certificate: %s", e)
|
||||||
return False
|
return False
|
||||||
@@ -369,14 +417,14 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||||||
def cast_to_int(s):
|
def cast_to_int(s):
|
||||||
try:
|
try:
|
||||||
return int(s)
|
return int(s)
|
||||||
except ValueError:
|
except (ValueError, TypeError):
|
||||||
return -1
|
return 0
|
||||||
|
|
||||||
def cast_to_float(s):
|
def cast_to_float(s):
|
||||||
try:
|
try:
|
||||||
return float(s)
|
return float(s)
|
||||||
except ValueError:
|
except (ValueError, TypeError):
|
||||||
return -1
|
return 0
|
||||||
|
|
||||||
def convert_xml_to_json(xml):
|
def convert_xml_to_json(xml):
|
||||||
o = xmltodict.parse(xml)
|
o = xmltodict.parse(xml)
|
||||||
@@ -446,4 +494,71 @@ def sanitize(string):
|
|||||||
if string:
|
if string:
|
||||||
return unicode(string).replace('<','<').replace('>','>')
|
return unicode(string).replace('<','<').replace('>','>')
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
def is_ip_public(host):
|
||||||
|
ip_address = get_ip(host)
|
||||||
|
ip = IP(ip_address)
|
||||||
|
if ip.iptype() == 'PUBLIC':
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_ip(host):
|
||||||
|
from plexpy import logger
|
||||||
|
ip_address = ''
|
||||||
|
try:
|
||||||
|
socket.inet_aton(host)
|
||||||
|
ip_address = host
|
||||||
|
except socket.error:
|
||||||
|
try:
|
||||||
|
ip_address = socket.gethostbyname(host)
|
||||||
|
logger.debug(u"IP Checker :: Resolved %s to %s." % (host, ip_address))
|
||||||
|
except:
|
||||||
|
logger.error(u"IP Checker :: Bad IP or hostname provided.")
|
||||||
|
|
||||||
|
return ip_address
|
||||||
|
|
||||||
|
# Taken from SickRage
|
||||||
|
def anon_url(*url):
|
||||||
|
"""
|
||||||
|
Return a URL string consisting of the Anonymous redirect URL and an arbitrary number of values appended.
|
||||||
|
"""
|
||||||
|
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
|
||||||
|
|
||||||
|
def uploadToImgur(imgPath, imgTitle=''):
|
||||||
|
from plexpy import logger
|
||||||
|
|
||||||
|
client_id = '743b1a443ccd2b0'
|
||||||
|
img_url = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(imgPath, 'rb') as imgFile:
|
||||||
|
img = imgFile.read()
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"PlexPy Helpers :: Unable to read image file for Imgur: %s" % e)
|
||||||
|
return img_url
|
||||||
|
|
||||||
|
headers = {'Authorization': 'Client-ID %s' % client_id}
|
||||||
|
data = {'type': 'base64',
|
||||||
|
'image': base64.b64encode(img)}
|
||||||
|
if imgTitle:
|
||||||
|
data['title'] = imgTitle.encode('utf-8')
|
||||||
|
data['name'] = imgTitle.encode('utf-8') + '.jpg'
|
||||||
|
|
||||||
|
try:
|
||||||
|
request = urllib2.Request('https://api.imgur.com/3/image', headers=headers, data=urllib.urlencode(data))
|
||||||
|
response = urllib2.urlopen(request)
|
||||||
|
response = json.loads(response.read())
|
||||||
|
|
||||||
|
if response.get('status') == 200:
|
||||||
|
logger.debug(u"PlexPy Helpers :: Image uploaded to Imgur.")
|
||||||
|
img_url = response.get('data').get('link', '')
|
||||||
|
elif response.get('status') >= 400 and response.get('status') < 500:
|
||||||
|
logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur: %s" % response.reason)
|
||||||
|
else:
|
||||||
|
logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur.")
|
||||||
|
except urllib2.HTTPError as e:
|
||||||
|
logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur: %s" % e)
|
||||||
|
|
||||||
|
return img_url
|
@@ -16,10 +16,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger, helpers
|
|
||||||
from httplib import HTTPSConnection
|
from httplib import HTTPSConnection
|
||||||
from httplib import HTTPConnection
|
from httplib import HTTPConnection
|
||||||
import ssl
|
import ssl
|
||||||
|
from plexpy import logger, helpers
|
||||||
|
|
||||||
|
|
||||||
class HTTPHandler(object):
|
class HTTPHandler(object):
|
||||||
@@ -44,7 +44,8 @@ class HTTPHandler(object):
|
|||||||
headers=None,
|
headers=None,
|
||||||
output_format='raw',
|
output_format='raw',
|
||||||
return_type=False,
|
return_type=False,
|
||||||
no_token=False):
|
no_token=False,
|
||||||
|
timeout=20):
|
||||||
|
|
||||||
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
|
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
|
||||||
|
|
||||||
@@ -56,12 +57,12 @@ class HTTPHandler(object):
|
|||||||
if proto.upper() == 'HTTPS':
|
if proto.upper() == 'HTTPS':
|
||||||
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
|
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
|
||||||
context = ssl._create_unverified_context()
|
context = ssl._create_unverified_context()
|
||||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=20, context=context)
|
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout, context=context)
|
||||||
logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
|
logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
|
||||||
else:
|
else:
|
||||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=20)
|
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout)
|
||||||
else:
|
else:
|
||||||
handler = HTTPConnection(host=self.host, port=self.port, timeout=20)
|
handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout)
|
||||||
|
|
||||||
token_string = ''
|
token_string = ''
|
||||||
if not no_token:
|
if not no_token:
|
||||||
|
@@ -18,62 +18,86 @@ import plexpy
|
|||||||
|
|
||||||
def update_section_ids():
|
def update_section_ids():
|
||||||
from plexpy import pmsconnect, activity_pinger
|
from plexpy import pmsconnect, activity_pinger
|
||||||
import threading
|
#import threading
|
||||||
|
|
||||||
plexpy.CONFIG.UPDATE_SECTION_IDS = -1
|
plexpy.CONFIG.UPDATE_SECTION_IDS = -1
|
||||||
|
|
||||||
logger.info(u"PlexPy Libraries :: Updating section_id's in database.")
|
#logger.debug(u"PlexPy Libraries :: Disabling monitoring while update in progress.")
|
||||||
|
#plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
|
||||||
logger.debug(u"PlexPy Libraries :: Disabling monitoring while update in progress.")
|
# hours=0, minutes=0, seconds=0)
|
||||||
plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
|
#plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||||
hours=0, minutes=0, seconds=0)
|
# hours=0, minutes=0, seconds=0)
|
||||||
plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
#plexpy.schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=0)
|
# hours=0, minutes=0, seconds=0)
|
||||||
plexpy.schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
|
||||||
hours=0, minutes=0, seconds=0)
|
|
||||||
|
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = 'SELECT id, rating_key FROM session_history_metadata WHERE section_id IS NULL'
|
query = 'SELECT id, rating_key, grandparent_rating_key, media_type ' \
|
||||||
result = monitor_db.select(query=query)
|
'FROM session_history_metadata WHERE section_id IS NULL'
|
||||||
|
history_results = monitor_db.select(query=query)
|
||||||
|
query = 'SELECT section_id, section_type FROM library_sections'
|
||||||
|
library_results = monitor_db.select(query=query)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"PlexPy Libraries :: Unable to execute database query for update_section_ids: %s." % e)
|
logger.warn(u"PlexPy Libraries :: Unable to execute database query for update_section_ids: %s." % e)
|
||||||
|
|
||||||
logger.warn(u"PlexPy Libraries :: Unable to update section_id's in database.")
|
logger.warn(u"PlexPy Libraries :: Unable to update section_id's in database.")
|
||||||
plexpy.CONFIG.__setattr__('UPDATE_SECTION_IDS', 1)
|
plexpy.CONFIG.UPDATE_SECTION_IDS = 1
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
#logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
||||||
plexpy.initialize_scheduler()
|
#plexpy.initialize_scheduler()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if not history_results:
|
||||||
|
plexpy.CONFIG.UPDATE_SECTION_IDS = 0
|
||||||
|
plexpy.CONFIG.write()
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(u"PlexPy Libraries :: Updating section_id's in database.")
|
||||||
|
|
||||||
# Add thread filter to the logger
|
# Add thread filter to the logger
|
||||||
logger.debug(u"PlexPy Libraries :: Disabling logging in the current thread while update in progress.")
|
#logger.debug(u"PlexPy Libraries :: Disabling logging in the current thread while update in progress.")
|
||||||
thread_filter = logger.NoThreadFilter(threading.current_thread().name)
|
#thread_filter = logger.NoThreadFilter(threading.current_thread().name)
|
||||||
for handler in logger.logger.handlers:
|
#for handler in logger.logger.handlers:
|
||||||
handler.addFilter(thread_filter)
|
# handler.addFilter(thread_filter)
|
||||||
|
|
||||||
|
# Get rating_key: section_id mapping pairs
|
||||||
|
key_mappings = {}
|
||||||
|
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
for library in library_results:
|
||||||
|
section_id = library['section_id']
|
||||||
|
section_type = library['section_type']
|
||||||
|
|
||||||
|
if section_type != 'photo':
|
||||||
|
library_children = pms_connect.get_library_children_details(section_id=section_id,
|
||||||
|
section_type=section_type)
|
||||||
|
if library_children:
|
||||||
|
children_list = library_children['childern_list']
|
||||||
|
key_mappings.update({child['rating_key']:child['section_id'] for child in children_list})
|
||||||
|
else:
|
||||||
|
logger.warn(u"PlexPy Libraries :: Unable to get a list of library items for section_id %s." % section_id)
|
||||||
|
|
||||||
error_keys = set()
|
error_keys = set()
|
||||||
for item in result:
|
for item in history_results:
|
||||||
id = item['id']
|
rating_key = item['grandparent_rating_key'] if item['media_type'] != 'movie' else item['rating_key']
|
||||||
rating_key = item['rating_key']
|
section_id = key_mappings.get(str(rating_key), None)
|
||||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
|
|
||||||
|
if section_id:
|
||||||
if metadata:
|
try:
|
||||||
metadata = metadata['metadata']
|
section_keys = {'id': item['id']}
|
||||||
section_keys = {'id': id}
|
section_values = {'section_id': section_id}
|
||||||
section_values = {'section_id': metadata['section_id']}
|
monitor_db.upsert('session_history_metadata', key_dict=section_keys, value_dict=section_values)
|
||||||
monitor_db.upsert('session_history_metadata', key_dict=section_keys, value_dict=section_values)
|
except:
|
||||||
|
error_keys.add(item['rating_key'])
|
||||||
else:
|
else:
|
||||||
error_keys.add(rating_key)
|
error_keys.add(item['rating_key'])
|
||||||
|
|
||||||
# Remove thread filter from the logger
|
# Remove thread filter from the logger
|
||||||
for handler in logger.logger.handlers:
|
#for handler in logger.logger.handlers:
|
||||||
handler.removeFilter(thread_filter)
|
# handler.removeFilter(thread_filter)
|
||||||
logger.debug(u"PlexPy Libraries :: Re-enabling logging in the current thread.")
|
#logger.debug(u"PlexPy Libraries :: Re-enabling logging in the current thread.")
|
||||||
|
|
||||||
if error_keys:
|
if error_keys:
|
||||||
logger.info(u"PlexPy Libraries :: Updated all section_id's in database except for rating_keys: %s." %
|
logger.info(u"PlexPy Libraries :: Updated all section_id's in database except for rating_keys: %s." %
|
||||||
@@ -81,11 +105,11 @@ def update_section_ids():
|
|||||||
else:
|
else:
|
||||||
logger.info(u"PlexPy Libraries :: Updated all section_id's in database.")
|
logger.info(u"PlexPy Libraries :: Updated all section_id's in database.")
|
||||||
|
|
||||||
plexpy.CONFIG.__setattr__('UPDATE_SECTION_IDS', 0)
|
plexpy.CONFIG.UPDATE_SECTION_IDS = 0
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
#logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
||||||
plexpy.initialize_scheduler()
|
#plexpy.initialize_scheduler()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -109,6 +133,9 @@ class Libraries(object):
|
|||||||
'library_sections.custom_thumb_url AS custom_thumb',
|
'library_sections.custom_thumb_url AS custom_thumb',
|
||||||
'library_sections.art',
|
'library_sections.art',
|
||||||
'COUNT(session_history.id) AS plays',
|
'COUNT(session_history.id) AS plays',
|
||||||
|
'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \
|
||||||
|
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
|
||||||
|
session_history.paused_counter END) AS duration',
|
||||||
'MAX(session_history.started) AS last_accessed',
|
'MAX(session_history.started) AS last_accessed',
|
||||||
'MAX(session_history.id) AS id',
|
'MAX(session_history.id) AS id',
|
||||||
'session_history_metadata.full_title AS last_played',
|
'session_history_metadata.full_title AS last_played',
|
||||||
@@ -176,6 +203,7 @@ class Libraries(object):
|
|||||||
'library_thumb': library_thumb,
|
'library_thumb': library_thumb,
|
||||||
'library_art': item['art'],
|
'library_art': item['art'],
|
||||||
'plays': item['plays'],
|
'plays': item['plays'],
|
||||||
|
'duration': item['duration'],
|
||||||
'last_accessed': item['last_accessed'],
|
'last_accessed': item['last_accessed'],
|
||||||
'id': item['id'],
|
'id': item['id'],
|
||||||
'last_played': item['last_played'],
|
'last_played': item['last_played'],
|
||||||
@@ -512,52 +540,27 @@ class Libraries(object):
|
|||||||
def get_details(self, section_id=None):
|
def get_details(self, section_id=None):
|
||||||
from plexpy import pmsconnect
|
from plexpy import pmsconnect
|
||||||
|
|
||||||
monitor_db = database.MonitorDatabase()
|
default_return = {'section_id': None,
|
||||||
|
'section_name': 'Local',
|
||||||
|
'section_type': '',
|
||||||
|
'library_thumb': common.DEFAULT_COVER_THUMB,
|
||||||
|
'library_art': '',
|
||||||
|
'count': 0,
|
||||||
|
'parent_count': 0,
|
||||||
|
'child_count': 0,
|
||||||
|
'do_notify': 0,
|
||||||
|
'do_notify_created': 0,
|
||||||
|
'keep_history': 0
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
if not section_id:
|
||||||
if section_id:
|
return default_return
|
||||||
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
|
||||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
|
||||||
'do_notify, do_notify_created, keep_history ' \
|
|
||||||
'FROM library_sections ' \
|
|
||||||
'WHERE section_id = ? '
|
|
||||||
result = monitor_db.select(query, args=[section_id])
|
|
||||||
else:
|
|
||||||
result = []
|
|
||||||
except Exception as e:
|
|
||||||
logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e)
|
|
||||||
result = []
|
|
||||||
|
|
||||||
if result:
|
def get_library_details(section_id=section_id):
|
||||||
library_details = {}
|
monitor_db = database.MonitorDatabase()
|
||||||
for item in result:
|
|
||||||
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
|
||||||
library_thumb = item['custom_thumb']
|
|
||||||
elif item['library_thumb']:
|
|
||||||
library_thumb = item['library_thumb']
|
|
||||||
else:
|
|
||||||
library_thumb = common.DEFAULT_COVER_THUMB
|
|
||||||
|
|
||||||
library_details = {'section_id': item['section_id'],
|
|
||||||
'section_name': item['section_name'],
|
|
||||||
'section_type': item['section_type'],
|
|
||||||
'library_thumb': library_thumb,
|
|
||||||
'library_art': item['art'],
|
|
||||||
'count': item['count'],
|
|
||||||
'parent_count': item['parent_count'],
|
|
||||||
'child_count': item['child_count'],
|
|
||||||
'do_notify': item['do_notify'],
|
|
||||||
'do_notify_created': item['do_notify_created'],
|
|
||||||
'keep_history': item['keep_history']
|
|
||||||
}
|
|
||||||
return library_details
|
|
||||||
else:
|
|
||||||
logger.warn(u"PlexPy Libraries :: Unable to retrieve library from local database. Requesting library list refresh.")
|
|
||||||
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
|
|
||||||
try:
|
try:
|
||||||
if section_id:
|
if str(section_id).isdigit():
|
||||||
# Refresh libraries
|
|
||||||
pmsconnect.refresh_libraries()
|
|
||||||
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
||||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
||||||
'do_notify, do_notify_created, keep_history ' \
|
'do_notify, do_notify_created, keep_history ' \
|
||||||
@@ -566,12 +569,12 @@ class Libraries(object):
|
|||||||
result = monitor_db.select(query, args=[section_id])
|
result = monitor_db.select(query, args=[section_id])
|
||||||
else:
|
else:
|
||||||
result = []
|
result = []
|
||||||
except:
|
except Exception as e:
|
||||||
logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e)
|
logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e)
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
|
library_details = {}
|
||||||
if result:
|
if result:
|
||||||
library_details = {}
|
|
||||||
for item in result:
|
for item in result:
|
||||||
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
||||||
library_thumb = item['custom_thumb']
|
library_thumb = item['custom_thumb']
|
||||||
@@ -592,22 +595,28 @@ class Libraries(object):
|
|||||||
'do_notify_created': item['do_notify_created'],
|
'do_notify_created': item['do_notify_created'],
|
||||||
'keep_history': item['keep_history']
|
'keep_history': item['keep_history']
|
||||||
}
|
}
|
||||||
|
return library_details
|
||||||
|
|
||||||
|
library_details = get_library_details(section_id=section_id)
|
||||||
|
|
||||||
|
if library_details:
|
||||||
|
return library_details
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warn(u"PlexPy Libraries :: Unable to retrieve library from local database. Requesting library list refresh.")
|
||||||
|
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
|
||||||
|
pmsconnect.refresh_libraries()
|
||||||
|
|
||||||
|
library_details = get_library_details(section_id=section_id)
|
||||||
|
|
||||||
|
if library_details:
|
||||||
return library_details
|
return library_details
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Returning 'Local' library.")
|
||||||
# If there is no library data we must return something
|
# If there is no library data we must return something
|
||||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
# Use "Local" library to retain compatibility with PlexWatch database value
|
||||||
return {'section_id': None,
|
return default_return
|
||||||
'section_name': 'Local',
|
|
||||||
'section_type': '',
|
|
||||||
'library_thumb': common.DEFAULT_COVER_THUMB,
|
|
||||||
'library_art': '',
|
|
||||||
'count': 0,
|
|
||||||
'parent_count': 0,
|
|
||||||
'child_count': 0,
|
|
||||||
'do_notify': 0,
|
|
||||||
'do_notify_created': 0,
|
|
||||||
'keep_history': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_watch_time_stats(self, section_id=None):
|
def get_watch_time_stats(self, section_id=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
@@ -856,3 +865,21 @@ class Libraries(object):
|
|||||||
return 'Unable to delete media info table cache, section_id not valid.'
|
return 'Unable to delete media info table cache, section_id not valid.'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"PlexPy Libraries :: Unable to delete media info table cache: %s." % e)
|
logger.warn(u"PlexPy Libraries :: Unable to delete media info table cache: %s." % e)
|
||||||
|
|
||||||
|
def delete_duplicate_libraries(self):
|
||||||
|
from plexpy import plextv
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
# Refresh the PMS_URL to make sure the server_id is updated
|
||||||
|
plextv.get_real_pms_url()
|
||||||
|
|
||||||
|
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(u"PlexPy Libraries :: Deleting libraries where server_id does not match %s." % server_id)
|
||||||
|
monitor_db.action('DELETE FROM library_sections WHERE server_id != ?', [server_id])
|
||||||
|
|
||||||
|
return 'Deleted duplicate libraries from the database.'
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"PlexPy Libraries :: Unable to delete duplicate libraries: %s." % e)
|
@@ -18,10 +18,14 @@ import re
|
|||||||
import os
|
import os
|
||||||
import plexpy
|
import plexpy
|
||||||
|
|
||||||
def get_log_tail(window=20, parsed=True):
|
def get_log_tail(window=20, parsed=True, log_type="server"):
|
||||||
|
|
||||||
if plexpy.CONFIG.PMS_LOGS_FOLDER:
|
if plexpy.CONFIG.PMS_LOGS_FOLDER:
|
||||||
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Server.log')
|
log_file = ""
|
||||||
|
if log_type == "server":
|
||||||
|
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Server.log')
|
||||||
|
elif log_type == "scanner":
|
||||||
|
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Scanner.log')
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@@ -14,11 +14,13 @@
|
|||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import arrow
|
import urllib
|
||||||
|
|
||||||
from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect
|
from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect, datafactory
|
||||||
import plexpy
|
import plexpy
|
||||||
|
|
||||||
|
|
||||||
@@ -48,157 +50,201 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
for agent in notifiers.available_notification_agents():
|
for agent in notifiers.available_notification_agents():
|
||||||
if agent['on_play'] and notify_action == 'play':
|
if agent['on_play'] and notify_action == 'play':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_stop'] and notify_action == 'stop' \
|
elif agent['on_stop'] and notify_action == 'stop' \
|
||||||
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT):
|
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT):
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_pause'] and notify_action == 'pause' \
|
elif agent['on_pause'] and notify_action == 'pause' \
|
||||||
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_resume'] and notify_action == 'resume' \
|
elif agent['on_resume'] and notify_action == 'resume' \
|
||||||
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_buffer'] and notify_action == 'buffer':
|
elif agent['on_buffer'] and notify_action == 'buffer':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_watched'] and notify_action == 'watched':
|
elif agent['on_watched'] and notify_action == 'watched':
|
||||||
# Get the current states for notifications from our db
|
# Get the current states for notifications from our db
|
||||||
notify_states = get_notify_state(session=stream_data)
|
notify_states = get_notify_state(session=stream_data)
|
||||||
|
|
||||||
# If there is nothing in the notify_log for our agent id but it is enabled we should notify
|
# If there is nothing in the notify_log for our agent id but it is enabled we should notify
|
||||||
if not any(d['agent_id'] == agent['id'] for d in notify_states):
|
if not any(d['agent_id'] == agent['id'] and d['notify_action'] == notify_action for d in notify_states):
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
else:
|
agent_info=agent,
|
||||||
# Check in our notify log if the notification has already been sent
|
notify_strings=notify_strings,
|
||||||
for notify_state in notify_states:
|
metadata=metadata)
|
||||||
if not notify_state['on_watched'] and (notify_state['agent_id'] == agent['id']):
|
|
||||||
# Build and send notification
|
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
|
||||||
subject=notify_strings[0],
|
|
||||||
body=notify_strings[1],
|
|
||||||
notify_action=notify_action,
|
|
||||||
script_args=notify_strings[2])
|
|
||||||
|
|
||||||
# Set the notification state in the db
|
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
|
||||||
|
|
||||||
elif (stream_data['media_type'] == 'track' and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
|
elif (stream_data['media_type'] == 'track' and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
|
||||||
|
|
||||||
for agent in notifiers.available_notification_agents():
|
for agent in notifiers.available_notification_agents():
|
||||||
if agent['on_play'] and notify_action == 'play':
|
if agent['on_play'] and notify_action == 'play':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_stop'] and notify_action == 'stop':
|
elif agent['on_stop'] and notify_action == 'stop':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_pause'] and notify_action == 'pause':
|
elif agent['on_pause'] and notify_action == 'pause':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_resume'] and notify_action == 'resume':
|
elif agent['on_resume'] and notify_action == 'resume':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif agent['on_buffer'] and notify_action == 'buffer':
|
elif agent['on_buffer'] and notify_action == 'buffer':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings, metadata = build_notify_text(session=stream_data, notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
notify_action=notify_action,
|
notify_action=notify_action,
|
||||||
script_args=notify_strings[2])
|
metadata=metadata)
|
||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
set_notify_state(session=stream_data,
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
elif stream_data['media_type'] == 'clip':
|
elif stream_data['media_type'] == 'clip':
|
||||||
pass
|
pass
|
||||||
@@ -211,141 +257,150 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
|
|
||||||
def notify_timeline(timeline_data=None, notify_action=None):
|
def notify_timeline(timeline_data=None, notify_action=None):
|
||||||
if timeline_data and notify_action:
|
if timeline_data and notify_action:
|
||||||
if (timeline_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
|
for agent in notifiers.available_notification_agents():
|
||||||
or ((timeline_data['media_type'] == 'show' or timeline_data['media_type'] == 'episode') \
|
if agent['on_created'] and notify_action == 'created':
|
||||||
and plexpy.CONFIG.TV_NOTIFY_ENABLE) \
|
# Build and send notification
|
||||||
or ((timeline_data['media_type'] == 'artist' or timeline_data['media_type'] == 'track') \
|
notify_strings, metadata = build_notify_text(timeline=timeline_data, notify_action=notify_action)
|
||||||
and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1],
|
||||||
|
script_args=notify_strings[2],
|
||||||
|
notify_action=notify_action,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
for agent in notifiers.available_notification_agents():
|
# Set the notification state in the db
|
||||||
if agent['on_created'] and notify_action == 'created':
|
set_notify_state(session=timeline_data,
|
||||||
# Build and send notification
|
notify_action=notify_action,
|
||||||
notify_strings = build_notify_text(timeline=timeline_data, state=notify_action)
|
agent_info=agent,
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notify_strings=notify_strings,
|
||||||
subject=notify_strings[0],
|
metadata=metadata)
|
||||||
body=notify_strings[1],
|
|
||||||
notify_action=notify_action,
|
|
||||||
script_args=notify_strings[2])
|
|
||||||
# Set the notification state in the db
|
|
||||||
set_notify_state(session=timeline_data, state=notify_action, agent_info=agent)
|
|
||||||
|
|
||||||
elif not timeline_data and notify_action:
|
elif not timeline_data and notify_action:
|
||||||
for agent in notifiers.available_notification_agents():
|
for agent in notifiers.available_notification_agents():
|
||||||
if agent['on_extdown'] and notify_action == 'extdown':
|
if agent['on_extdown'] and notify_action == 'extdown':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_server_notify_text(state=notify_action)
|
notify_strings = build_server_notify_text(notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
notify_action=notify_action,
|
script_args=notify_strings[2],
|
||||||
script_args=notify_strings[2])
|
notify_action=notify_action)
|
||||||
|
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session={},
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata={})
|
||||||
|
|
||||||
if agent['on_intdown'] and notify_action == 'intdown':
|
if agent['on_intdown'] and notify_action == 'intdown':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_server_notify_text(state=notify_action)
|
notify_strings = build_server_notify_text(notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
notify_action=notify_action,
|
script_args=notify_strings[2],
|
||||||
script_args=notify_strings[2])
|
notify_action=notify_action)
|
||||||
|
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session={},
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata={})
|
||||||
|
|
||||||
if agent['on_extup'] and notify_action == 'extup':
|
if agent['on_extup'] and notify_action == 'extup':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_server_notify_text(state=notify_action)
|
notify_strings = build_server_notify_text(notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
notify_action=notify_action,
|
script_args=notify_strings[2],
|
||||||
script_args=notify_strings[2])
|
notify_action=notify_action)
|
||||||
|
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session={},
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata={})
|
||||||
|
|
||||||
if agent['on_intup'] and notify_action == 'intup':
|
if agent['on_intup'] and notify_action == 'intup':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_server_notify_text(state=notify_action)
|
notify_strings = build_server_notify_text(notify_action=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(agent_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1],
|
body=notify_strings[1],
|
||||||
notify_action=notify_action,
|
script_args=notify_strings[2],
|
||||||
script_args=notify_strings[2])
|
notify_action=notify_action)
|
||||||
|
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session={},
|
||||||
|
notify_action=notify_action,
|
||||||
|
agent_info=agent,
|
||||||
|
notify_strings=notify_strings,
|
||||||
|
metadata={})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug(u"PlexPy NotificationHandler :: Notify timeline called but incomplete data received.")
|
logger.debug(u"PlexPy NotificationHandler :: Notify timeline called but incomplete data received.")
|
||||||
|
|
||||||
|
|
||||||
def get_notify_state(session):
|
def get_notify_state(session):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id '
|
result = monitor_db.select('SELECT timestamp, notify_action, agent_id '
|
||||||
'FROM notify_log '
|
'FROM notify_log '
|
||||||
'WHERE session_key = ? '
|
'WHERE session_key = ? '
|
||||||
'AND rating_key = ? '
|
'AND rating_key = ? '
|
||||||
'AND user = ? '
|
'AND user_id = ? '
|
||||||
'ORDER BY id DESC',
|
'ORDER BY id DESC',
|
||||||
args=[session['session_key'], session['rating_key'], session['user']])
|
args=[session['session_key'], session['rating_key'], session['user_id']])
|
||||||
notify_states = []
|
notify_states = []
|
||||||
for item in result:
|
for item in result:
|
||||||
notify_state = {'on_play': item['on_play'],
|
notify_state = {'timestamp': item['timestamp'],
|
||||||
'on_stop': item['on_stop'],
|
'notify_action': item['notify_action'],
|
||||||
'on_pause': item['on_pause'],
|
|
||||||
'on_resume': item['on_resume'],
|
|
||||||
'on_buffer': item['on_buffer'],
|
|
||||||
'on_watched': item['on_watched'],
|
|
||||||
'agent_id': item['agent_id']}
|
'agent_id': item['agent_id']}
|
||||||
notify_states.append(notify_state)
|
notify_states.append(notify_state)
|
||||||
|
|
||||||
return notify_states
|
return notify_states
|
||||||
|
|
||||||
|
|
||||||
def get_notify_state_timeline(timeline):
|
def set_notify_state(session, notify_action, agent_info, notify_strings, metadata):
|
||||||
monitor_db = database.MonitorDatabase()
|
|
||||||
result = monitor_db.select('SELECT on_created, agent_id '
|
|
||||||
'FROM notify_log '
|
|
||||||
'WHERE rating_key = ? '
|
|
||||||
'ORDER BY id DESC',
|
|
||||||
args=[timeline['rating_key']])
|
|
||||||
notify_states = []
|
|
||||||
for item in result:
|
|
||||||
notify_state = {'on_created': item['on_created'],
|
|
||||||
'agent_id': item['agent_id']}
|
|
||||||
notify_states.append(notify_state)
|
|
||||||
|
|
||||||
return notify_states
|
if notify_action and agent_info:
|
||||||
|
|
||||||
|
|
||||||
def set_notify_state(session, state, agent_info):
|
|
||||||
|
|
||||||
if session and state and agent_info:
|
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if state == 'play':
|
if notify_strings[2]:
|
||||||
values = {'on_play': int(time.time())}
|
script_args = '[' + ', '.join(notify_strings[2]) + ']'
|
||||||
elif state == 'stop':
|
|
||||||
values = {'on_stop': int(time.time())}
|
|
||||||
elif state == 'pause':
|
|
||||||
values = {'on_pause': int(time.time())}
|
|
||||||
elif state == 'resume':
|
|
||||||
values = {'on_resume': int(time.time())}
|
|
||||||
elif state == 'buffer':
|
|
||||||
values = {'on_buffer': int(time.time())}
|
|
||||||
elif state == 'watched':
|
|
||||||
values = {'on_watched': int(time.time())}
|
|
||||||
elif state == 'created':
|
|
||||||
values = {'on_created': int(time.time())}
|
|
||||||
else:
|
else:
|
||||||
return
|
script_args = None
|
||||||
|
|
||||||
if state == 'created':
|
keys = {'timestamp': int(time.time()),
|
||||||
keys = {'rating_key': session['rating_key'],
|
'session_key': session.get('session_key', None),
|
||||||
'agent_id': agent_info['id'],
|
'rating_key': session.get('rating_key', None),
|
||||||
'agent_name': agent_info['name']}
|
'user_id': session.get('user_id', None),
|
||||||
else:
|
'agent_id': agent_info['id'],
|
||||||
keys = {'session_key': session['session_key'],
|
'notify_action': notify_action}
|
||||||
'rating_key': session['rating_key'],
|
|
||||||
'user_id': session['user_id'],
|
values = {'parent_rating_key': session.get('parent_rating_key', None),
|
||||||
'user': session['user'],
|
'grandparent_rating_key': session.get('grandparent_rating_key', None),
|
||||||
'agent_id': agent_info['id'],
|
'user': session.get('user', None),
|
||||||
'agent_name': agent_info['name']}
|
'agent_name': agent_info['name'],
|
||||||
|
'subject_text': notify_strings[0],
|
||||||
|
'body_text': notify_strings[1],
|
||||||
|
'script_args': script_args,
|
||||||
|
'poster_url': metadata.get('poster_url', None)}
|
||||||
|
|
||||||
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
|
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
|
||||||
else:
|
else:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to set notify state.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to set notify state.")
|
||||||
|
|
||||||
|
|
||||||
def build_notify_text(session=None, timeline=None, state=None):
|
def build_notify_text(session=None, timeline=None, notify_action=None):
|
||||||
|
# Get time formats
|
||||||
|
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')
|
||||||
|
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')
|
||||||
|
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','').replace('a','').replace('A','')
|
||||||
|
|
||||||
# Get the server name
|
# Get the server name
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
server_name = plexpy.CONFIG.PMS_NAME
|
||||||
|
|
||||||
@@ -355,7 +410,7 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
|
|
||||||
if server_times:
|
if server_times:
|
||||||
updated_at = server_times[0]['updated_at']
|
updated_at = server_times[0]['updated_at']
|
||||||
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
|
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
|
||||||
else:
|
else:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to retrieve server uptime.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to retrieve server uptime.")
|
||||||
server_uptime = 'N/A'
|
server_uptime = 'N/A'
|
||||||
@@ -434,81 +489,93 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
else:
|
else:
|
||||||
full_title = metadata['title']
|
full_title = metadata['title']
|
||||||
|
|
||||||
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
user = ''
|
|
||||||
platform = ''
|
|
||||||
player = ''
|
|
||||||
ip_address = 'N/A'
|
|
||||||
stream_duration = 0
|
|
||||||
view_offset = 0
|
|
||||||
container = ''
|
|
||||||
video_codec = ''
|
|
||||||
video_bitrate = ''
|
|
||||||
video_width = ''
|
|
||||||
video_height = ''
|
|
||||||
video_resolution = ''
|
|
||||||
video_framerate = ''
|
|
||||||
aspect_ratio = ''
|
|
||||||
audio_codec = ''
|
|
||||||
audio_channels = ''
|
|
||||||
transcode_decision = ''
|
|
||||||
video_decision = ''
|
|
||||||
audio_decision = ''
|
|
||||||
transcode_container = ''
|
|
||||||
transcode_video_codec = ''
|
|
||||||
transcode_video_width = ''
|
|
||||||
transcode_video_height = ''
|
|
||||||
transcode_audio_codec = ''
|
|
||||||
transcode_audio_channels = ''
|
|
||||||
|
|
||||||
# Session values
|
# Session values
|
||||||
if session:
|
if session is None:
|
||||||
# Generate a combined transcode decision value
|
session = {}
|
||||||
video_decision = session['video_decision'].title()
|
|
||||||
audio_decision = session['audio_decision'].title()
|
|
||||||
|
|
||||||
if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode':
|
# Generate a combined transcode decision value
|
||||||
transcode_decision = 'Transcode'
|
if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode':
|
||||||
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
|
transcode_decision = 'Transcode'
|
||||||
transcode_decision = 'Direct Stream'
|
elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy':
|
||||||
else:
|
transcode_decision = 'Direct Stream'
|
||||||
transcode_decision = 'Direct Play'
|
else:
|
||||||
|
transcode_decision = 'Direct Play'
|
||||||
if state != 'play':
|
|
||||||
if session['paused_counter']:
|
if notify_action != 'play':
|
||||||
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) -
|
stream_duration = int((time.time() -
|
||||||
helpers.cast_to_float(session['paused_counter'])) / 60)
|
helpers.cast_to_int(session.get('started', 0)) -
|
||||||
else:
|
helpers.cast_to_int(session.get('paused_counter', 0))) / 60)
|
||||||
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60)
|
else:
|
||||||
|
stream_duration = 0
|
||||||
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
|
|
||||||
user = session['friendly_name']
|
|
||||||
platform = session['platform']
|
|
||||||
player = session['player']
|
|
||||||
ip_address = session['ip_address'] if session['ip_address'] else 'N/A'
|
|
||||||
container = session['container']
|
|
||||||
video_codec = session['video_codec']
|
|
||||||
video_bitrate = session['bitrate']
|
|
||||||
video_width = session['width']
|
|
||||||
video_height = session['height']
|
|
||||||
video_resolution = session['video_resolution']
|
|
||||||
video_framerate = session['video_framerate']
|
|
||||||
aspect_ratio = session['aspect_ratio']
|
|
||||||
audio_codec = session['audio_codec']
|
|
||||||
audio_channels = session['audio_channels']
|
|
||||||
transcode_container = session['transcode_container']
|
|
||||||
transcode_video_codec = session['transcode_video_codec']
|
|
||||||
transcode_video_width = session['transcode_width']
|
|
||||||
transcode_video_height = session['transcode_height']
|
|
||||||
transcode_audio_codec = session['transcode_audio_codec']
|
|
||||||
transcode_audio_channels = session['transcode_audio_channels']
|
|
||||||
|
|
||||||
|
view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 0))
|
||||||
|
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
|
||||||
progress_percent = helpers.get_percent(view_offset, duration)
|
progress_percent = helpers.get_percent(view_offset, duration)
|
||||||
|
remaining_duration = duration - view_offset
|
||||||
|
|
||||||
|
# Get media IDs from guid and build URLs
|
||||||
|
if 'imdb://' in metadata['guid']:
|
||||||
|
metadata['imdb_id'] = metadata['guid'].split('imdb://')[1].split('?')[0]
|
||||||
|
metadata['imdb_url'] = 'https://www.imdb.com/title/' + metadata['imdb_id']
|
||||||
|
metadata['trakt_url'] = 'https://trakt.tv/search/imdb/' + metadata['imdb_id']
|
||||||
|
|
||||||
|
if 'thetvdb://' in metadata['guid']:
|
||||||
|
metadata['thetvdb_id'] = metadata['guid'].split('thetvdb://')[1].split('/')[0]
|
||||||
|
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id']
|
||||||
|
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show'
|
||||||
|
|
||||||
|
elif 'thetvdbdvdorder://' in metadata['guid']:
|
||||||
|
metadata['thetvdb_id'] = metadata['guid'].split('thetvdbdvdorder://')[1].split('/')[0]
|
||||||
|
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id']
|
||||||
|
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show'
|
||||||
|
|
||||||
|
if 'themoviedb://' in metadata['guid']:
|
||||||
|
if metadata['media_type'] == 'movie':
|
||||||
|
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('?')[0]
|
||||||
|
metadata['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + metadata['themoviedb_id']
|
||||||
|
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=movie'
|
||||||
|
|
||||||
|
elif metadata['media_type'] == 'show' or metadata['media_type'] == 'episode':
|
||||||
|
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('/')[0]
|
||||||
|
metadata['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + metadata['themoviedb_id']
|
||||||
|
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=show'
|
||||||
|
|
||||||
|
if 'lastfm://' in metadata['guid']:
|
||||||
|
metadata['lastfm_id'] = metadata['guid'].split('lastfm://')[1].rsplit('/', 1)[0]
|
||||||
|
metadata['lastfm_url'] = 'https://www.last.fm/music/' + metadata['lastfm_id']
|
||||||
|
|
||||||
|
if metadata['media_type'] == 'movie' or metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
|
||||||
|
thumb = metadata['thumb']
|
||||||
|
poster_key = metadata['rating_key']
|
||||||
|
poster_title = metadata['title']
|
||||||
|
elif metadata['media_type'] == 'episode':
|
||||||
|
thumb = metadata['grandparent_thumb']
|
||||||
|
poster_key = metadata['grandparent_rating_key']
|
||||||
|
poster_title = metadata['grandparent_title']
|
||||||
|
elif metadata['media_type'] == 'track':
|
||||||
|
thumb = metadata['parent_thumb']
|
||||||
|
poster_key = metadata['parent_rating_key']
|
||||||
|
poster_title = metadata['parent_title']
|
||||||
|
else:
|
||||||
|
thumb = None
|
||||||
|
|
||||||
|
if thumb:
|
||||||
|
# Try to retrieve a poster_url from the database
|
||||||
|
data_factory = datafactory.DataFactory()
|
||||||
|
poster_url = data_factory.get_poster_url(rating_key=poster_key)
|
||||||
|
|
||||||
|
# If no previous poster_url
|
||||||
|
if not poster_url and plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS:
|
||||||
|
# Retrieve the poster from Plex and cache to file
|
||||||
|
urllib.urlretrieve(plexpy.CONFIG.PMS_URL + thumb + '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN,
|
||||||
|
os.path.join(plexpy.CONFIG.CACHE_DIR, 'cache-poster.jpg'))
|
||||||
|
# Upload thumb to Imgur and get link
|
||||||
|
poster_url = helpers.uploadToImgur(os.path.join(plexpy.CONFIG.CACHE_DIR, 'cache-poster.jpg'), poster_title)
|
||||||
|
|
||||||
|
metadata['poster_url'] = poster_url
|
||||||
|
|
||||||
# Fix metadata params for notify recently added grandparent
|
# Fix metadata params for notify recently added grandparent
|
||||||
if state == 'created' and plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
|
if notify_action == 'created' and plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
|
||||||
show_name = metadata['title']
|
show_name = metadata['title']
|
||||||
episode_name = ''
|
episode_name = ''
|
||||||
artist_name = metadata['title']
|
artist_name = metadata['title']
|
||||||
@@ -521,40 +588,49 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
album_name = metadata['parent_title']
|
album_name = metadata['parent_title']
|
||||||
track_name = metadata['title']
|
track_name = metadata['title']
|
||||||
|
|
||||||
available_params = {'server_name': server_name,
|
available_params = {# Global paramaters
|
||||||
|
'server_name': server_name,
|
||||||
'server_uptime': server_uptime,
|
'server_uptime': server_uptime,
|
||||||
|
'action': notify_action.title(),
|
||||||
|
'datestamp': arrow.now().format(date_format),
|
||||||
|
'timestamp': arrow.now().format(time_format),
|
||||||
|
# Stream parameters
|
||||||
'streams': stream_count,
|
'streams': stream_count,
|
||||||
'action': state,
|
'user': session.get('friendly_name',''),
|
||||||
'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')),
|
'platform': session.get('platform',''),
|
||||||
'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')),
|
'player': session.get('player',''),
|
||||||
'user': user,
|
'ip_address': session.get('ip_address','N/A'),
|
||||||
'platform': platform,
|
|
||||||
'player': player,
|
|
||||||
'ip_address': ip_address,
|
|
||||||
'media_type': metadata['media_type'],
|
|
||||||
'stream_duration': stream_duration,
|
'stream_duration': stream_duration,
|
||||||
'remaining_duration': duration - view_offset,
|
'stream_time': arrow.get(stream_duration * 60).format(duration_format),
|
||||||
'progress': view_offset,
|
'remaining_duration': remaining_duration,
|
||||||
|
'remaining_time': arrow.get(remaining_duration * 60).format(duration_format),
|
||||||
|
'progress_duration': view_offset,
|
||||||
|
'progress_time': arrow.get(view_offset * 60).format(duration_format),
|
||||||
'progress_percent': progress_percent,
|
'progress_percent': progress_percent,
|
||||||
'container': container,
|
'container': session.get('container',''),
|
||||||
'video_codec': video_codec,
|
'video_codec': session.get('video_codec',''),
|
||||||
'video_bitrate': video_bitrate,
|
'video_bitrate': session.get('bitrate',''),
|
||||||
'video_width': video_width,
|
'video_width': session.get('width',''),
|
||||||
'video_height': video_height,
|
'video_height': session.get('height',''),
|
||||||
'video_resolution': video_resolution,
|
'video_resolution': session.get('video_resolution',''),
|
||||||
'video_framerate': video_framerate,
|
'video_framerate': session.get('video_framerate',''),
|
||||||
'aspect_ratio': aspect_ratio,
|
'aspect_ratio': session.get('aspect_ratio',''),
|
||||||
'audio_codec': audio_codec,
|
'audio_codec': session.get('audio_codec',''),
|
||||||
'audio_channels': audio_channels,
|
'audio_channels': session.get('audio_channels',''),
|
||||||
'transcode_decision': transcode_decision,
|
'transcode_decision': transcode_decision,
|
||||||
'video_decision': video_decision,
|
'video_decision': session.get('video_decision','').title(),
|
||||||
'audio_decision': audio_decision,
|
'audio_decision': session.get('audio_decision','').title(),
|
||||||
'transcode_container': transcode_container,
|
'transcode_container': session.get('transcode_container',''),
|
||||||
'transcode_video_codec': transcode_video_codec,
|
'transcode_video_codec': session.get('transcode_video_codec',''),
|
||||||
'transcode_video_width': transcode_video_width,
|
'transcode_video_width': session.get('transcode_width',''),
|
||||||
'transcode_video_height': transcode_video_height,
|
'transcode_video_height': session.get('transcode_height',''),
|
||||||
'transcode_audio_codec': transcode_audio_codec,
|
'transcode_audio_codec': session.get('transcode_audio_codec',''),
|
||||||
'transcode_audio_channels': transcode_audio_channels,
|
'transcode_audio_channels': session.get('transcode_audio_channels',''),
|
||||||
|
'session_key': session.get('session_key',''),
|
||||||
|
'user_id': session.get('user_id',''),
|
||||||
|
'machine_id': session.get('machine_id',''),
|
||||||
|
# Metadata parameters
|
||||||
|
'media_type': metadata['media_type'],
|
||||||
'title': full_title,
|
'title': full_title,
|
||||||
'library_name': metadata['library_name'],
|
'library_name': metadata['library_name'],
|
||||||
'show_name': show_name,
|
'show_name': show_name,
|
||||||
@@ -566,6 +642,8 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
'season_num00': metadata['parent_media_index'].zfill(2),
|
'season_num00': metadata['parent_media_index'].zfill(2),
|
||||||
'episode_num': metadata['media_index'].zfill(1),
|
'episode_num': metadata['media_index'].zfill(1),
|
||||||
'episode_num00': metadata['media_index'].zfill(2),
|
'episode_num00': metadata['media_index'].zfill(2),
|
||||||
|
'track_num': metadata['media_index'].zfill(1),
|
||||||
|
'track_num00': metadata['media_index'].zfill(2),
|
||||||
'year': metadata['year'],
|
'year': metadata['year'],
|
||||||
'studio': metadata['studio'],
|
'studio': metadata['studio'],
|
||||||
'content_rating': metadata['content_rating'],
|
'content_rating': metadata['content_rating'],
|
||||||
@@ -576,7 +654,20 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
'summary': metadata['summary'],
|
'summary': metadata['summary'],
|
||||||
'tagline': metadata['tagline'],
|
'tagline': metadata['tagline'],
|
||||||
'rating': metadata['rating'],
|
'rating': metadata['rating'],
|
||||||
'duration': duration
|
'duration': duration,
|
||||||
|
'poster_url': metadata.get('poster_url',''),
|
||||||
|
'imdb_id': metadata.get('imdb_id',''),
|
||||||
|
'imdb_url': metadata.get('imdb_url',''),
|
||||||
|
'thetvdb_id': metadata.get('thetvdb_id',''),
|
||||||
|
'thetvdb_url': metadata.get('thetvdb_url',''),
|
||||||
|
'themoviedb_id': metadata.get('themoviedb_id',''),
|
||||||
|
'themoviedb_url': metadata.get('themoviedb_url',''),
|
||||||
|
'lastfm_url': metadata.get('lastfm_url',''),
|
||||||
|
'trakt_url': metadata.get('trakt_url',''),
|
||||||
|
'section_id': metadata['section_id'],
|
||||||
|
'rating_key': metadata['rating_key'],
|
||||||
|
'parent_rating_key': metadata['parent_rating_key'],
|
||||||
|
'grandparent_rating_key': metadata['grandparent_rating_key']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Default subject text
|
# Default subject text
|
||||||
@@ -585,10 +676,6 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
# Default scripts args
|
# Default scripts args
|
||||||
script_args = []
|
script_args = []
|
||||||
|
|
||||||
# Regex to match {param} but not "{param}"
|
|
||||||
params_to_quote = re.compile(r'(?<!\")([\{][^}]+[\}])(?!\"\})')
|
|
||||||
script_args_text = re.sub(params_to_quote, r'"\g<0>"', script_args_text)
|
|
||||||
|
|
||||||
if script_args_text:
|
if script_args_text:
|
||||||
try:
|
try:
|
||||||
script_args = [unicode(arg).format(**available_params) for arg in script_args_text.split()]
|
script_args = [unicode(arg).format(**available_params) for arg in script_args_text.split()]
|
||||||
@@ -597,11 +684,11 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse custom script arguments %s. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse custom script arguments %s. Using fallback." % e)
|
||||||
|
|
||||||
if state == 'play':
|
if notify_action == 'play':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s (%s) is watching %s' % (session['friendly_name'],
|
body_text = '%s (%s) started playing %s' % (session['friendly_name'],
|
||||||
session['player'],
|
session['player'],
|
||||||
full_title)
|
full_title)
|
||||||
|
|
||||||
if on_start_subject and on_start_body:
|
if on_start_subject and on_start_body:
|
||||||
try:
|
try:
|
||||||
@@ -618,10 +705,10 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
elif state == 'stop':
|
elif notify_action == 'stop':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s (%s) has stopped %s' % (session['friendly_name'],
|
body_text = '%s (%s) has stopped %s' % (session['friendly_name'],
|
||||||
session['player'],
|
session['player'],
|
||||||
@@ -642,10 +729,10 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
elif state == 'pause':
|
elif notify_action == 'pause':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s (%s) has paused %s' % (session['friendly_name'],
|
body_text = '%s (%s) has paused %s' % (session['friendly_name'],
|
||||||
session['player'],
|
session['player'],
|
||||||
@@ -666,10 +753,10 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
elif state == 'resume':
|
elif notify_action == 'resume':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s (%s) has resumed %s' % (session['friendly_name'],
|
body_text = '%s (%s) has resumed %s' % (session['friendly_name'],
|
||||||
session['player'],
|
session['player'],
|
||||||
@@ -690,10 +777,10 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
elif state == 'buffer':
|
elif notify_action == 'buffer':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s (%s) is buffering %s' % (session['friendly_name'],
|
body_text = '%s (%s) is buffering %s' % (session['friendly_name'],
|
||||||
session['player'],
|
session['player'],
|
||||||
@@ -714,10 +801,10 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
elif state == 'watched':
|
elif notify_action == 'watched':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s (%s) has watched %s' % (session['friendly_name'],
|
body_text = '%s (%s) has watched %s' % (session['friendly_name'],
|
||||||
session['player'],
|
session['player'],
|
||||||
@@ -738,10 +825,10 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
elif state == 'created':
|
elif notify_action == 'created':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = '%s was recently added to Plex.' % full_title
|
body_text = '%s was recently added to Plex.' % full_title
|
||||||
|
|
||||||
@@ -760,14 +847,18 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args], metadata
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def build_server_notify_text(state=None):
|
def build_server_notify_text(notify_action=None):
|
||||||
|
# Get time formats
|
||||||
|
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')
|
||||||
|
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')
|
||||||
|
|
||||||
# Get the server name
|
# Get the server name
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
server_name = plexpy.CONFIG.PMS_NAME
|
||||||
|
|
||||||
@@ -777,11 +868,13 @@ def build_server_notify_text(state=None):
|
|||||||
|
|
||||||
if server_times:
|
if server_times:
|
||||||
updated_at = server_times[0]['updated_at']
|
updated_at = server_times[0]['updated_at']
|
||||||
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
|
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
|
||||||
else:
|
else:
|
||||||
logger.error(u"PlexPy NotificationHandler :: Unable to retrieve server uptime.")
|
logger.error(u"PlexPy NotificationHandler :: Unable to retrieve server uptime.")
|
||||||
server_uptime = 'N/A'
|
server_uptime = 'N/A'
|
||||||
|
|
||||||
|
pattern = re.compile('\n*<tv>[^>]+.</tv>\n*|\n*<movie>[^>]+.</movie>\n*|\n*?<music>[^>]+.</music>\n*', re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
on_extdown_subject = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT
|
on_extdown_subject = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT
|
||||||
on_extdown_body = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT
|
on_extdown_body = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT
|
||||||
on_intdown_subject = plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT
|
on_intdown_subject = plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT
|
||||||
@@ -790,13 +883,14 @@ def build_server_notify_text(state=None):
|
|||||||
on_extup_body = plexpy.CONFIG.NOTIFY_ON_EXTUP_BODY_TEXT
|
on_extup_body = plexpy.CONFIG.NOTIFY_ON_EXTUP_BODY_TEXT
|
||||||
on_intup_subject = plexpy.CONFIG.NOTIFY_ON_INTUP_SUBJECT_TEXT
|
on_intup_subject = plexpy.CONFIG.NOTIFY_ON_INTUP_SUBJECT_TEXT
|
||||||
on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT
|
on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT
|
||||||
script_args_text = plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT
|
script_args_text = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT))
|
||||||
|
|
||||||
available_params = {'server_name': server_name,
|
available_params = {# Global paramaters
|
||||||
|
'server_name': server_name,
|
||||||
'server_uptime': server_uptime,
|
'server_uptime': server_uptime,
|
||||||
'action': state,
|
'action': notify_action.title(),
|
||||||
'datestamp': time.strftime(helpers.parse_js_date(plexpy.CONFIG.DATE_FORMAT)),
|
'datestamp': arrow.now().format(date_format),
|
||||||
'timestamp': time.strftime(helpers.parse_js_date(plexpy.CONFIG.TIME_FORMAT))}
|
'timestamp': arrow.now().format(time_format)}
|
||||||
|
|
||||||
# Default text
|
# Default text
|
||||||
subject_text = 'PlexPy (%s)' % server_name
|
subject_text = 'PlexPy (%s)' % server_name
|
||||||
@@ -804,10 +898,6 @@ def build_server_notify_text(state=None):
|
|||||||
# Default scripts args
|
# Default scripts args
|
||||||
script_args = []
|
script_args = []
|
||||||
|
|
||||||
# Regex to match {param} but not "{param}"
|
|
||||||
params_to_quote = re.compile(r'(?<!\")([\{][^}]+[\}])(?!\"\})')
|
|
||||||
script_args_text = re.sub(params_to_quote, r'"\g<0>"', script_args_text)
|
|
||||||
|
|
||||||
if script_args_text:
|
if script_args_text:
|
||||||
try:
|
try:
|
||||||
script_args = [unicode(arg).format(**available_params) for arg in script_args_text.split()]
|
script_args = [unicode(arg).format(**available_params) for arg in script_args_text.split()]
|
||||||
@@ -816,7 +906,7 @@ def build_server_notify_text(state=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse custom script arguments %s. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse custom script arguments %s. Using fallback." % e)
|
||||||
|
|
||||||
if state == 'extdown':
|
if notify_action == 'extdown':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = 'The Plex Media Server remote access is down.'
|
body_text = 'The Plex Media Server remote access is down.'
|
||||||
|
|
||||||
@@ -839,7 +929,7 @@ def build_server_notify_text(state=None):
|
|||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args]
|
||||||
|
|
||||||
elif state == 'intdown':
|
elif notify_action == 'intdown':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = 'The Plex Media Server is down.'
|
body_text = 'The Plex Media Server is down.'
|
||||||
|
|
||||||
@@ -861,7 +951,7 @@ def build_server_notify_text(state=None):
|
|||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args]
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args]
|
||||||
if state == 'extup':
|
if notify_action == 'extup':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = 'The Plex Media Server remote access is back up.'
|
body_text = 'The Plex Media Server remote access is back up.'
|
||||||
|
|
||||||
@@ -883,7 +973,7 @@ def build_server_notify_text(state=None):
|
|||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args]
|
||||||
else:
|
else:
|
||||||
return [subject_text, body_text, script_args]
|
return [subject_text, body_text, script_args]
|
||||||
elif state == 'intup':
|
elif notify_action == 'intup':
|
||||||
# Default body text
|
# Default body text
|
||||||
body_text = 'The Plex Media Server is back up.'
|
body_text = 'The Plex Media Server is back up.'
|
||||||
|
|
||||||
@@ -912,4 +1002,4 @@ def build_server_notify_text(state=None):
|
|||||||
|
|
||||||
def strip_tag(data):
|
def strip_tag(data):
|
||||||
p = re.compile(r'<.*?>')
|
p = re.compile(r'<.*?>')
|
||||||
return p.sub('', data)
|
return p.sub('', data)
|
@@ -34,7 +34,7 @@ from pynma import pynma
|
|||||||
import gntp.notifier
|
import gntp.notifier
|
||||||
import oauth2 as oauth
|
import oauth2 as oauth
|
||||||
import pythontwitter as twitter
|
import pythontwitter as twitter
|
||||||
import pythonfacebook as facebook
|
import pythonfacebook as facebook
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import logger, helpers, request
|
from plexpy import logger, helpers, request
|
||||||
@@ -58,7 +58,7 @@ AGENT_IDS = {"Growl": 0,
|
|||||||
"Scripts": 15,
|
"Scripts": 15,
|
||||||
"Facebook": 16}
|
"Facebook": 16}
|
||||||
|
|
||||||
|
|
||||||
def available_notification_agents():
|
def available_notification_agents():
|
||||||
agents = [{'name': 'Growl',
|
agents = [{'name': 'Growl',
|
||||||
'id': AGENT_IDS['Growl'],
|
'id': AGENT_IDS['Growl'],
|
||||||
@@ -358,59 +358,59 @@ def available_notification_agents():
|
|||||||
return agents
|
return agents
|
||||||
|
|
||||||
|
|
||||||
def get_notification_agent_config(config_id):
|
def get_notification_agent_config(agent_id):
|
||||||
if str(config_id).isdigit():
|
if str(agent_id).isdigit():
|
||||||
config_id = int(config_id)
|
agent_id = int(agent_id)
|
||||||
|
|
||||||
if config_id == 0:
|
if agent_id == 0:
|
||||||
growl = GROWL()
|
growl = GROWL()
|
||||||
return growl.return_config_options()
|
return growl.return_config_options()
|
||||||
elif config_id == 1:
|
elif agent_id == 1:
|
||||||
prowl = PROWL()
|
prowl = PROWL()
|
||||||
return prowl.return_config_options()
|
return prowl.return_config_options()
|
||||||
elif config_id == 2:
|
elif agent_id == 2:
|
||||||
xbmc = XBMC()
|
xbmc = XBMC()
|
||||||
return xbmc.return_config_options()
|
return xbmc.return_config_options()
|
||||||
elif config_id == 3:
|
elif agent_id == 3:
|
||||||
plex = Plex()
|
plex = Plex()
|
||||||
return plex.return_config_options()
|
return plex.return_config_options()
|
||||||
elif config_id == 4:
|
elif agent_id == 4:
|
||||||
nma = NMA()
|
nma = NMA()
|
||||||
return nma.return_config_options()
|
return nma.return_config_options()
|
||||||
elif config_id == 5:
|
elif agent_id == 5:
|
||||||
pushalot = PUSHALOT()
|
pushalot = PUSHALOT()
|
||||||
return pushalot.return_config_options()
|
return pushalot.return_config_options()
|
||||||
elif config_id == 6:
|
elif agent_id == 6:
|
||||||
pushbullet = PUSHBULLET()
|
pushbullet = PUSHBULLET()
|
||||||
return pushbullet.return_config_options()
|
return pushbullet.return_config_options()
|
||||||
elif config_id == 7:
|
elif agent_id == 7:
|
||||||
pushover = PUSHOVER()
|
pushover = PUSHOVER()
|
||||||
return pushover.return_config_options()
|
return pushover.return_config_options()
|
||||||
elif config_id == 8:
|
elif agent_id == 8:
|
||||||
osx_notify = OSX_NOTIFY()
|
osx_notify = OSX_NOTIFY()
|
||||||
return osx_notify.return_config_options()
|
return osx_notify.return_config_options()
|
||||||
elif config_id == 9:
|
elif agent_id == 9:
|
||||||
boxcar = BOXCAR()
|
boxcar = BOXCAR()
|
||||||
return boxcar.return_config_options()
|
return boxcar.return_config_options()
|
||||||
elif config_id == 10:
|
elif agent_id == 10:
|
||||||
email = Email()
|
email = Email()
|
||||||
return email.return_config_options()
|
return email.return_config_options()
|
||||||
elif config_id == 11:
|
elif agent_id == 11:
|
||||||
tweet = TwitterNotifier()
|
tweet = TwitterNotifier()
|
||||||
return tweet.return_config_options()
|
return tweet.return_config_options()
|
||||||
elif config_id == 12:
|
elif agent_id == 12:
|
||||||
iftttClient = IFTTT()
|
iftttClient = IFTTT()
|
||||||
return iftttClient.return_config_options()
|
return iftttClient.return_config_options()
|
||||||
elif config_id == 13:
|
elif agent_id == 13:
|
||||||
telegramClient = TELEGRAM()
|
telegramClient = TELEGRAM()
|
||||||
return telegramClient.return_config_options()
|
return telegramClient.return_config_options()
|
||||||
elif config_id == 14:
|
elif agent_id == 14:
|
||||||
slackClient = SLACK()
|
slackClient = SLACK()
|
||||||
return slackClient.return_config_options()
|
return slackClient.return_config_options()
|
||||||
elif config_id == 15:
|
elif agent_id == 15:
|
||||||
script = Scripts()
|
script = Scripts()
|
||||||
return script.return_config_options()
|
return script.return_config_options()
|
||||||
elif config_id == 16:
|
elif agent_id == 16:
|
||||||
facebook = FacebookNotifier()
|
facebook = FacebookNotifier()
|
||||||
return facebook.return_config_options()
|
return facebook.return_config_options()
|
||||||
else:
|
else:
|
||||||
@@ -419,61 +419,61 @@ def get_notification_agent_config(config_id):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def send_notification(config_id, subject, body, **kwargs):
|
def send_notification(agent_id, subject, body, **kwargs):
|
||||||
if str(config_id).isdigit():
|
if str(agent_id).isdigit():
|
||||||
config_id = int(config_id)
|
agent_id = int(agent_id)
|
||||||
|
|
||||||
if config_id == 0:
|
if agent_id == 0:
|
||||||
growl = GROWL()
|
growl = GROWL()
|
||||||
growl.notify(message=body, event=subject)
|
growl.notify(message=body, event=subject)
|
||||||
elif config_id == 1:
|
elif agent_id == 1:
|
||||||
prowl = PROWL()
|
prowl = PROWL()
|
||||||
prowl.notify(message=body, event=subject)
|
prowl.notify(message=body, event=subject)
|
||||||
elif config_id == 2:
|
elif agent_id == 2:
|
||||||
xbmc = XBMC()
|
xbmc = XBMC()
|
||||||
xbmc.notify(subject=subject, message=body)
|
xbmc.notify(subject=subject, message=body)
|
||||||
elif config_id == 3:
|
elif agent_id == 3:
|
||||||
plex = Plex()
|
plex = Plex()
|
||||||
plex.notify(subject=subject, message=body)
|
plex.notify(subject=subject, message=body)
|
||||||
elif config_id == 4:
|
elif agent_id == 4:
|
||||||
nma = NMA()
|
nma = NMA()
|
||||||
nma.notify(subject=subject, message=body)
|
nma.notify(subject=subject, message=body)
|
||||||
elif config_id == 5:
|
elif agent_id == 5:
|
||||||
pushalot = PUSHALOT()
|
pushalot = PUSHALOT()
|
||||||
pushalot.notify(message=body, event=subject)
|
pushalot.notify(message=body, event=subject)
|
||||||
elif config_id == 6:
|
elif agent_id == 6:
|
||||||
pushbullet = PUSHBULLET()
|
pushbullet = PUSHBULLET()
|
||||||
pushbullet.notify(message=body, subject=subject)
|
pushbullet.notify(message=body, subject=subject)
|
||||||
elif config_id == 7:
|
elif agent_id == 7:
|
||||||
pushover = PUSHOVER()
|
pushover = PUSHOVER()
|
||||||
pushover.notify(message=body, event=subject)
|
pushover.notify(message=body, event=subject)
|
||||||
elif config_id == 8:
|
elif agent_id == 8:
|
||||||
osx_notify = OSX_NOTIFY()
|
osx_notify = OSX_NOTIFY()
|
||||||
osx_notify.notify(title=subject, text=body)
|
osx_notify.notify(title=subject, text=body)
|
||||||
elif config_id == 9:
|
elif agent_id == 9:
|
||||||
boxcar = BOXCAR()
|
boxcar = BOXCAR()
|
||||||
boxcar.notify(title=subject, message=body)
|
boxcar.notify(title=subject, message=body)
|
||||||
elif config_id == 10:
|
elif agent_id == 10:
|
||||||
email = Email()
|
email = Email()
|
||||||
email.notify(subject=subject, message=body)
|
email.notify(subject=subject, message=body)
|
||||||
elif config_id == 11:
|
elif agent_id == 11:
|
||||||
tweet = TwitterNotifier()
|
tweet = TwitterNotifier()
|
||||||
tweet.notify(subject=subject, message=body)
|
tweet.notify(subject=subject, message=body)
|
||||||
elif config_id == 12:
|
elif agent_id == 12:
|
||||||
iftttClient = IFTTT()
|
iftttClient = IFTTT()
|
||||||
iftttClient.notify(subject=subject, message=body)
|
iftttClient.notify(subject=subject, message=body)
|
||||||
elif config_id == 13:
|
elif agent_id == 13:
|
||||||
telegramClient = TELEGRAM()
|
telegramClient = TELEGRAM()
|
||||||
telegramClient.notify(message=body, event=subject)
|
telegramClient.notify(message=body, event=subject)
|
||||||
elif config_id == 14:
|
elif agent_id == 14:
|
||||||
slackClient = SLACK()
|
slackClient = SLACK()
|
||||||
slackClient.notify(message=body, event=subject)
|
slackClient.notify(message=body, event=subject)
|
||||||
elif config_id == 15:
|
elif agent_id == 15:
|
||||||
scripts = Scripts()
|
scripts = Scripts()
|
||||||
scripts.notify(message=body, subject=subject, **kwargs)
|
scripts.notify(message=body, subject=subject, **kwargs)
|
||||||
elif config_id == 16:
|
elif agent_id == 16:
|
||||||
facebook = FacebookNotifier()
|
facebook = FacebookNotifier()
|
||||||
facebook.notify(subject=subject, message=body)
|
facebook.notify(subject=subject, message=body, **kwargs)
|
||||||
else:
|
else:
|
||||||
logger.debug(u"PlexPy Notifiers :: Unknown agent id received.")
|
logger.debug(u"PlexPy Notifiers :: Unknown agent id received.")
|
||||||
else:
|
else:
|
||||||
@@ -1054,6 +1054,7 @@ class PUSHOVER(object):
|
|||||||
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
|
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
|
||||||
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
|
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
|
||||||
self.sound = plexpy.CONFIG.PUSHOVER_SOUND
|
self.sound = plexpy.CONFIG.PUSHOVER_SOUND
|
||||||
|
self.html_support = plexpy.CONFIG.PUSHOVER_HTML_SUPPORT
|
||||||
|
|
||||||
def conf(self, options):
|
def conf(self, options):
|
||||||
return cherrypy.config['config'].get('Pushover', options)
|
return cherrypy.config['config'].get('Pushover', options)
|
||||||
@@ -1069,6 +1070,7 @@ class PUSHOVER(object):
|
|||||||
'title': event.encode("utf-8"),
|
'title': event.encode("utf-8"),
|
||||||
'message': message.encode("utf-8"),
|
'message': message.encode("utf-8"),
|
||||||
'sound': plexpy.CONFIG.PUSHOVER_SOUND,
|
'sound': plexpy.CONFIG.PUSHOVER_SOUND,
|
||||||
|
'html': plexpy.CONFIG.PUSHOVER_HTML_SUPPORT,
|
||||||
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
|
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
|
||||||
|
|
||||||
http_handler.request("POST",
|
http_handler.request("POST",
|
||||||
@@ -1095,11 +1097,12 @@ class PUSHOVER(object):
|
|||||||
# For uniformity reasons not removed
|
# For uniformity reasons not removed
|
||||||
return
|
return
|
||||||
|
|
||||||
def test(self, keys, priority, sound):
|
def test(self, keys, priority, sound, html_support):
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self.keys = keys
|
self.keys = keys
|
||||||
self.priority = priority
|
self.priority = priority
|
||||||
self.sound = sound
|
self.sound = sound
|
||||||
|
self.html_support = html_support
|
||||||
|
|
||||||
self.notify('Main Screen Activate', 'Test Message')
|
self.notify('Main Screen Activate', 'Test Message')
|
||||||
|
|
||||||
@@ -1151,6 +1154,12 @@ class PUSHOVER(object):
|
|||||||
'description': 'Set the notification sound. Leave blank for the default sound.',
|
'description': 'Set the notification sound. Leave blank for the default sound.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': self.get_sounds()
|
'select_options': self.get_sounds()
|
||||||
|
},
|
||||||
|
{'label': 'Enable HTML Support',
|
||||||
|
'value': self.html_support,
|
||||||
|
'name': 'pushover_html_support',
|
||||||
|
'description': 'Style your messages using these HTML Tags: b, i, u, a[href], font[color]',
|
||||||
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1165,14 +1174,20 @@ class TwitterNotifier(object):
|
|||||||
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
|
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.consumer_key = "2LdJKXHDUwJtjYBsdwJisIOsh"
|
self.access_token = plexpy.CONFIG.TWITTER_ACCESS_TOKEN
|
||||||
self.consumer_secret = "QWbUcZzAIiL4zbDCIhy2EdUkV8yEEav3qMdo5y3FugxCFelWrA"
|
self.access_token_secret = plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET
|
||||||
|
self.consumer_key = plexpy.CONFIG.TWITTER_CONSUMER_KEY
|
||||||
|
self.consumer_secret = plexpy.CONFIG.TWITTER_CONSUMER_SECRET
|
||||||
|
self.incl_subject = plexpy.CONFIG.TWITTER_INCL_SUBJECT
|
||||||
|
|
||||||
def notify(self, subject, message):
|
def notify(self, subject, message):
|
||||||
if not subject or not message:
|
if not subject or not message:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self._send_tweet(subject + ': ' + message)
|
if self.incl_subject:
|
||||||
|
self._send_tweet(subject + ': ' + message)
|
||||||
|
else:
|
||||||
|
self._send_tweet(message)
|
||||||
|
|
||||||
def test_notify(self):
|
def test_notify(self):
|
||||||
return self._send_tweet("This is a test notification from PlexPy at " + helpers.now())
|
return self._send_tweet("This is a test notification from PlexPy at " + helpers.now())
|
||||||
@@ -1191,16 +1206,16 @@ class TwitterNotifier(object):
|
|||||||
else:
|
else:
|
||||||
request_token = dict(parse_qsl(content))
|
request_token = dict(parse_qsl(content))
|
||||||
|
|
||||||
plexpy.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
|
plexpy.CONFIG.TWITTER_ACCESS_TOKEN = request_token['oauth_token']
|
||||||
plexpy.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret']
|
plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET = request_token['oauth_token_secret']
|
||||||
|
|
||||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token']
|
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token']
|
||||||
|
|
||||||
def _get_credentials(self, key):
|
def _get_credentials(self, key):
|
||||||
request_token = {}
|
request_token = {}
|
||||||
|
|
||||||
request_token['oauth_token'] = plexpy.CONFIG.TWITTER_USERNAME
|
request_token['oauth_token'] = plexpy.CONFIG.TWITTER_ACCESS_TOKEN
|
||||||
request_token['oauth_token_secret'] = plexpy.CONFIG.TWITTER_PASSWORD
|
request_token['oauth_token_secret'] = plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET
|
||||||
request_token['oauth_callback_confirmed'] = 'true'
|
request_token['oauth_callback_confirmed'] = 'true'
|
||||||
|
|
||||||
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
|
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
|
||||||
@@ -1225,20 +1240,20 @@ class TwitterNotifier(object):
|
|||||||
else:
|
else:
|
||||||
# logger.info(u"PlexPy Notifiers :: Your Twitter Access Token key: %s" % access_token['oauth_token'])
|
# logger.info(u"PlexPy Notifiers :: Your Twitter Access Token key: %s" % access_token['oauth_token'])
|
||||||
# logger.info(u"PlexPy Notifiers :: Access Token secret: %s" % access_token['oauth_token_secret'])
|
# logger.info(u"PlexPy Notifiers :: Access Token secret: %s" % access_token['oauth_token_secret'])
|
||||||
plexpy.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
|
plexpy.CONFIG.TWITTER_ACCESS_TOKEN = access_token['oauth_token']
|
||||||
plexpy.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret']
|
plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET = access_token['oauth_token_secret']
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _send_tweet(self, message=None):
|
def _send_tweet(self, message=None):
|
||||||
username = self.consumer_key
|
consumer_key = self.consumer_key
|
||||||
password = self.consumer_secret
|
consumer_secret = self.consumer_secret
|
||||||
access_token_key = plexpy.CONFIG.TWITTER_USERNAME
|
access_token = self.access_token
|
||||||
access_token_secret = plexpy.CONFIG.TWITTER_PASSWORD
|
access_token_secret = self.access_token_secret
|
||||||
|
|
||||||
# logger.info(u"PlexPy Notifiers :: Sending tweet: " + message)
|
# logger.info(u"PlexPy Notifiers :: Sending tweet: " + message)
|
||||||
|
|
||||||
api = twitter.Api(username, password, access_token_key, access_token_secret)
|
api = twitter.Api(consumer_key, consumer_secret, access_token, access_token_secret)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api.PostUpdate(message)
|
api.PostUpdate(message)
|
||||||
@@ -1251,30 +1266,43 @@ class TwitterNotifier(object):
|
|||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'Instructions',
|
config_option = [{'label': 'Instructions',
|
||||||
'description': 'Step 1: Click the <strong>Request Authorization</strong> button below.<br>\
|
'description': 'Step 1: Visit <a href="https://apps.twitter.com/" target="_blank"> \
|
||||||
Step 2: Input the <strong>Authorization Key</strong> you received from Step 1 below.<br>\
|
Twitter Apps</a> to <strong>Create New App</strong>. A vaild "Website" is not required.<br>\
|
||||||
Step 3: Click the <strong>Verify Key</strong> button below.',
|
Step 2: Go to <strong>Keys and Access Tokens</strong> and click \
|
||||||
|
<strong>Create my access token</strong>.<br>\
|
||||||
|
Step 3: Fill in the <strong>Consumer Key</strong>, <strong>Consumer Secret</strong>, \
|
||||||
|
<strong>Access Token</strong>, and <strong>Access Token Secret</strong> below.',
|
||||||
'input_type': 'help'
|
'input_type': 'help'
|
||||||
},
|
},
|
||||||
{'label': 'Request Authorization',
|
{'label': 'Twitter Consumer Key',
|
||||||
'value': 'Request Authorization',
|
'value': self.consumer_key,
|
||||||
'name': 'twitterStep1',
|
'name': 'twitter_consumer_key',
|
||||||
'description': 'Request Twitter authorization. (Ensure you allow the browser pop-up).',
|
'description': 'Your Twitter consumer key.',
|
||||||
'input_type': 'button'
|
|
||||||
},
|
|
||||||
{'label': 'Authorization Key',
|
|
||||||
'value': '',
|
|
||||||
'name': 'twitter_key',
|
|
||||||
'description': 'Your Twitter authorization key.',
|
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Verify Key',
|
{'label': 'Twitter Consumer Secret',
|
||||||
'value': 'Verify Key',
|
'value': self.consumer_secret,
|
||||||
'name': 'twitterStep2',
|
'name': 'twitter_consumer_secret',
|
||||||
'description': 'Verify your Twitter authorization key.',
|
'description': 'Your Twitter consumer secret.',
|
||||||
'input_type': 'button'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'input_type': 'nosave'
|
{'label': 'Twitter Access Token',
|
||||||
|
'value': self.access_token,
|
||||||
|
'name': 'twitter_access_token',
|
||||||
|
'description': 'Your Twitter access token.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Twitter Access Token Secret',
|
||||||
|
'value': self.access_token_secret,
|
||||||
|
'name': 'twitter_access_token_secret',
|
||||||
|
'description': 'Your Twitter access token secret.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Include Subject Line',
|
||||||
|
'value': self.incl_subject,
|
||||||
|
'name': 'twitter_incl_subject',
|
||||||
|
'description': 'Include the subject line in the notifications.',
|
||||||
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1619,6 +1647,7 @@ class TELEGRAM(object):
|
|||||||
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
|
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
|
||||||
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
|
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
|
||||||
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
|
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
|
||||||
|
self.incl_subject = plexpy.CONFIG.TELEGRAM_INCL_SUBJECT
|
||||||
|
|
||||||
def conf(self, options):
|
def conf(self, options):
|
||||||
return cherrypy.config['config'].get('Telegram', options)
|
return cherrypy.config['config'].get('Telegram', options)
|
||||||
@@ -1629,8 +1658,13 @@ class TELEGRAM(object):
|
|||||||
|
|
||||||
http_handler = HTTPSConnection("api.telegram.org")
|
http_handler = HTTPSConnection("api.telegram.org")
|
||||||
|
|
||||||
|
if self.incl_subject:
|
||||||
|
text = event.encode('utf-8') + ': ' + message.encode("utf-8")
|
||||||
|
else:
|
||||||
|
text = message.encode("utf-8")
|
||||||
|
|
||||||
data = {'chat_id': self.chat_id,
|
data = {'chat_id': self.chat_id,
|
||||||
'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
|
'text': text}
|
||||||
|
|
||||||
http_handler.request("POST",
|
http_handler.request("POST",
|
||||||
"/bot%s/%s" % (self.bot_token, "sendMessage"),
|
"/bot%s/%s" % (self.bot_token, "sendMessage"),
|
||||||
@@ -1668,11 +1702,17 @@ class TELEGRAM(object):
|
|||||||
'description': 'Your Telegram bot token. Contact <a href="http://telegram.me/BotFather" target="_blank">@BotFather</a> on Telegram to get one.',
|
'description': 'Your Telegram bot token. Contact <a href="http://telegram.me/BotFather" target="_blank">@BotFather</a> on Telegram to get one.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Telegram Chat ID',
|
{'label': 'Telegram Chat ID, Group ID, or Channel Username',
|
||||||
'value': self.chat_id,
|
'value': self.chat_id,
|
||||||
'name': 'telegram_chat_id',
|
'name': 'telegram_chat_id',
|
||||||
'description': 'Your Telegram Chat ID, Group ID, or channel username. Contact <a href="http://telegram.me/myidbot" target="_blank">@myidbot</a> on Telegram to get an ID.',
|
'description': 'Your Telegram Chat ID, Group ID, or @channelusername. Contact <a href="http://telegram.me/myidbot" target="_blank">@myidbot</a> on Telegram to get an ID.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Include Subject Line',
|
||||||
|
'value': self.incl_subject,
|
||||||
|
'name': 'telegram_incl_subject',
|
||||||
|
'description': 'Include the subject line in the notifications.',
|
||||||
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1689,6 +1729,7 @@ class SLACK(object):
|
|||||||
self.channel = plexpy.CONFIG.SLACK_CHANNEL
|
self.channel = plexpy.CONFIG.SLACK_CHANNEL
|
||||||
self.username = plexpy.CONFIG.SLACK_USERNAME
|
self.username = plexpy.CONFIG.SLACK_USERNAME
|
||||||
self.icon_emoji = plexpy.CONFIG.SLACK_ICON_EMOJI
|
self.icon_emoji = plexpy.CONFIG.SLACK_ICON_EMOJI
|
||||||
|
self.incl_subject = plexpy.CONFIG.SLACK_INCL_SUBJECT
|
||||||
|
|
||||||
def conf(self, options):
|
def conf(self, options):
|
||||||
return cherrypy.config['config'].get('Slack', options)
|
return cherrypy.config['config'].get('Slack', options)
|
||||||
@@ -1698,7 +1739,12 @@ class SLACK(object):
|
|||||||
return
|
return
|
||||||
http_handler = HTTPSConnection("hooks.slack.com")
|
http_handler = HTTPSConnection("hooks.slack.com")
|
||||||
|
|
||||||
data = {'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
|
if self.incl_subject:
|
||||||
|
text = event.encode('utf-8') + ': ' + message.encode("utf-8")
|
||||||
|
else:
|
||||||
|
text = message.encode("utf-8")
|
||||||
|
|
||||||
|
data = {'text': text}
|
||||||
if self.channel != '': data['channel'] = self.channel
|
if self.channel != '': data['channel'] = self.channel
|
||||||
if self.username != '': data['username'] = self.username
|
if self.username != '': data['username'] = self.username
|
||||||
if self.icon_emoji != '':
|
if self.icon_emoji != '':
|
||||||
@@ -1736,10 +1782,10 @@ class SLACK(object):
|
|||||||
return self.notify('Main Screen Activate', 'Test Message')
|
return self.notify('Main Screen Activate', 'Test Message')
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'Slack Hook',
|
config_option = [{'label': 'Slack Webhook URL',
|
||||||
'value': self.slack_hook,
|
'value': self.slack_hook,
|
||||||
'name': 'slack_hook',
|
'name': 'slack_hook',
|
||||||
'description': 'Your Slack incoming webhook.',
|
'description': 'Your Slack incoming webhook URL.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Slack Channel',
|
{'label': 'Slack Channel',
|
||||||
@@ -1759,6 +1805,12 @@ class SLACK(object):
|
|||||||
'description': 'The icon you wish to show, use Slack emoji or image url. Leave blank for webhook integration default.',
|
'description': 'The icon you wish to show, use Slack emoji or image url. Leave blank for webhook integration default.',
|
||||||
'name': 'slack_icon_emoji',
|
'name': 'slack_icon_emoji',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Include Subject Line',
|
||||||
|
'value': self.incl_subject,
|
||||||
|
'name': 'slack_incl_subject',
|
||||||
|
'description': 'Include the subject line in the notifications.',
|
||||||
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1768,7 +1820,7 @@ class SLACK(object):
|
|||||||
class Scripts(object):
|
class Scripts(object):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.py', '.pyw', '.rb', '.sh')
|
self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.ps1', '.py', '.pyw', '.rb', '.sh')
|
||||||
|
|
||||||
def conf(self, options):
|
def conf(self, options):
|
||||||
return cherrypy.config['config'].get('Scripts', options)
|
return cherrypy.config['config'].get('Scripts', options)
|
||||||
@@ -1798,7 +1850,7 @@ class Scripts(object):
|
|||||||
|
|
||||||
return scripts
|
return scripts
|
||||||
|
|
||||||
def notify(self, subject='', message='', notify_action='', script_args=[], *args, **kwargs):
|
def notify(self, subject='', message='', notify_action='', script_args=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
subject(string, optional): Head text,
|
subject(string, optional): Head text,
|
||||||
@@ -1808,7 +1860,10 @@ class Scripts(object):
|
|||||||
"""
|
"""
|
||||||
logger.debug(u"PlexPy Notifiers :: Trying to run notify script, action: %s, arguments: %s" %
|
logger.debug(u"PlexPy Notifiers :: Trying to run notify script, action: %s, arguments: %s" %
|
||||||
(notify_action if notify_action else None, script_args if script_args else None))
|
(notify_action if notify_action else None, script_args if script_args else None))
|
||||||
|
|
||||||
|
if script_args is None:
|
||||||
|
script_args = []
|
||||||
|
|
||||||
if not plexpy.CONFIG.SCRIPTS_FOLDER:
|
if not plexpy.CONFIG.SCRIPTS_FOLDER:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1860,14 +1915,16 @@ class Scripts(object):
|
|||||||
|
|
||||||
name, ext = os.path.splitext(script)
|
name, ext = os.path.splitext(script)
|
||||||
|
|
||||||
if ext == '.py':
|
if ext == '.php':
|
||||||
prefix = 'python'
|
|
||||||
elif ext == '.pyw':
|
|
||||||
prefix = 'pythonw'
|
|
||||||
elif ext == '.php':
|
|
||||||
prefix = 'php'
|
prefix = 'php'
|
||||||
elif ext == '.pl':
|
elif ext == '.pl':
|
||||||
prefix = 'perl'
|
prefix = 'perl'
|
||||||
|
elif ext == '.ps1':
|
||||||
|
prefix = 'powershell -executionPolicy bypass -file'
|
||||||
|
elif ext == '.py':
|
||||||
|
prefix = 'python'
|
||||||
|
elif ext == '.pyw':
|
||||||
|
prefix = 'pythonw'
|
||||||
elif ext == '.rb':
|
elif ext == '.rb':
|
||||||
prefix = 'ruby'
|
prefix = 'ruby'
|
||||||
else:
|
else:
|
||||||
@@ -1877,7 +1934,7 @@ class Scripts(object):
|
|||||||
script = script.encode(plexpy.SYS_ENCODING, 'ignore')
|
script = script.encode(plexpy.SYS_ENCODING, 'ignore')
|
||||||
|
|
||||||
if prefix:
|
if prefix:
|
||||||
script = [prefix, script]
|
script = prefix.split() + [script]
|
||||||
else:
|
else:
|
||||||
script = [script]
|
script = [script]
|
||||||
|
|
||||||
@@ -2016,20 +2073,26 @@ class Scripts(object):
|
|||||||
|
|
||||||
return config_option
|
return config_option
|
||||||
|
|
||||||
|
|
||||||
class FacebookNotifier(object):
|
class FacebookNotifier(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.redirect_uri = plexpy.CONFIG.FACEBOOK_REDIRECT_URI
|
self.redirect_uri = plexpy.CONFIG.FACEBOOK_REDIRECT_URI
|
||||||
|
self.access_token = plexpy.CONFIG.FACEBOOK_TOKEN
|
||||||
self.app_id = plexpy.CONFIG.FACEBOOK_APP_ID
|
self.app_id = plexpy.CONFIG.FACEBOOK_APP_ID
|
||||||
self.app_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET
|
self.app_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET
|
||||||
self.group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
self.group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
||||||
|
self.incl_poster = plexpy.CONFIG.FACEBOOK_INCL_POSTER
|
||||||
|
self.incl_subject = plexpy.CONFIG.FACEBOOK_INCL_SUBJECT
|
||||||
|
|
||||||
def notify(self, subject, message):
|
def notify(self, subject, message, **kwargs):
|
||||||
if not subject or not message:
|
if not subject or not message:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self._post_facebook(subject + ': ' + message)
|
if self.incl_subject:
|
||||||
|
self._post_facebook(subject + ': ' + message, **kwargs)
|
||||||
|
else:
|
||||||
|
self._post_facebook(message, **kwargs)
|
||||||
|
|
||||||
def test_notify(self):
|
def test_notify(self):
|
||||||
return self._post_facebook(u"PlexPy Notifiers :: This is a test notification from PlexPy at " + helpers.now())
|
return self._post_facebook(u"PlexPy Notifiers :: This is a test notification from PlexPy at " + helpers.now())
|
||||||
@@ -2041,7 +2104,7 @@ class FacebookNotifier(object):
|
|||||||
|
|
||||||
def _get_credentials(self, code):
|
def _get_credentials(self, code):
|
||||||
logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook")
|
logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Request user access token
|
# Request user access token
|
||||||
api = facebook.GraphAPI(version='2.5')
|
api = facebook.GraphAPI(version='2.5')
|
||||||
@@ -2050,30 +2113,66 @@ class FacebookNotifier(object):
|
|||||||
app_id=self.app_id,
|
app_id=self.app_id,
|
||||||
app_secret=self.app_secret)
|
app_secret=self.app_secret)
|
||||||
access_token = response['access_token']
|
access_token = response['access_token']
|
||||||
|
|
||||||
# Request extended user access token
|
# Request extended user access token
|
||||||
api = facebook.GraphAPI(access_token=access_token, version='2.5')
|
api = facebook.GraphAPI(access_token=access_token, version='2.5')
|
||||||
response = api.extend_access_token(app_id=self.app_id,
|
response = api.extend_access_token(app_id=self.app_id,
|
||||||
app_secret=self.app_secret)
|
app_secret=self.app_secret)
|
||||||
access_token = response['access_token']
|
access_token = response['access_token']
|
||||||
|
|
||||||
plexpy.CONFIG.FACEBOOK_TOKEN = access_token
|
plexpy.CONFIG.FACEBOOK_TOKEN = access_token
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e)
|
logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _post_facebook(self, message=None):
|
def _post_facebook(self, message=None, **kwargs):
|
||||||
access_token = plexpy.CONFIG.FACEBOOK_TOKEN
|
if self.group_id:
|
||||||
group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
api = facebook.GraphAPI(access_token=self.access_token, version='2.5')
|
||||||
|
|
||||||
if group_id:
|
attachment = {}
|
||||||
api = facebook.GraphAPI(access_token=access_token, version='2.5')
|
|
||||||
|
if self.incl_poster and 'metadata' in kwargs:
|
||||||
|
metadata = kwargs['metadata']
|
||||||
|
poster_url = metadata.get('poster_url','')
|
||||||
|
|
||||||
|
if poster_url:
|
||||||
|
if metadata['media_type'] == 'movie' or metadata['media_type'] == 'show':
|
||||||
|
title = metadata['title']
|
||||||
|
subtitle = metadata['year']
|
||||||
|
rating_key = metadata['rating_key']
|
||||||
|
|
||||||
|
elif metadata['media_type'] == 'episode':
|
||||||
|
title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||||
|
subtitle = 'S%s %s E%s' % (metadata['parent_media_index'],
|
||||||
|
'\xc2\xb7'.decode('utf8'),
|
||||||
|
metadata['media_index'])
|
||||||
|
rating_key = metadata['rating_key']
|
||||||
|
|
||||||
|
elif metadata['media_type'] == 'artist':
|
||||||
|
title = metadata['title']
|
||||||
|
subtitle = ''
|
||||||
|
rating_key = metadata['rating_key']
|
||||||
|
|
||||||
|
elif metadata['media_type'] == 'track':
|
||||||
|
title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||||
|
subtitle = metadata['parent_title']
|
||||||
|
rating_key = metadata['parent_rating_key']
|
||||||
|
|
||||||
|
caption = 'View in Plex Web.'
|
||||||
|
|
||||||
|
# Build Facebook post attachment
|
||||||
|
attachment['link'] = 'http://app.plex.tv/web/app#!/server/' + plexpy.CONFIG.PMS_IDENTIFIER + \
|
||||||
|
'/details/%2Flibrary%2Fmetadata%2F' + rating_key
|
||||||
|
attachment['picture'] = poster_url
|
||||||
|
attachment['name'] = title
|
||||||
|
attachment['description'] = subtitle
|
||||||
|
attachment['caption'] = caption
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api.put_wall_post(profile_id=group_id, message=message)
|
api.put_wall_post(profile_id=self.group_id, message=message, attachment=attachment)
|
||||||
logger.info(u"PlexPy Notifiers :: Facebook notification sent.")
|
logger.info(u"PlexPy Notifiers :: Facebook notification sent.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"PlexPy Notifiers :: Error sending Facebook post: %s" % e)
|
logger.warn(u"PlexPy Notifiers :: Error sending Facebook post: %s" % e)
|
||||||
@@ -2088,12 +2187,16 @@ class FacebookNotifier(object):
|
|||||||
config_option = [{'label': 'Instructions',
|
config_option = [{'label': 'Instructions',
|
||||||
'description': '<strong>Facebook notifications are currently experimental!</strong><br><br> \
|
'description': '<strong>Facebook notifications are currently experimental!</strong><br><br> \
|
||||||
Step 1: Visit <a href="https://developers.facebook.com/apps/" target="_blank"> \
|
Step 1: Visit <a href="https://developers.facebook.com/apps/" target="_blank"> \
|
||||||
Facebook Developers</a> to create a new app using <strong>advanced setup</strong>.<br>\
|
Facebook Developers</a> to add a new app using <strong>basic setup</strong>.<br>\
|
||||||
Step 2: Go to <strong>Settings > Advanced</strong> and fill in \
|
Step 2: Go to <strong>Settings > Basic</strong> and fill in a \
|
||||||
|
<strong>Contact Email</strong>.<br>\
|
||||||
|
Step 3: Go to <strong>Settings > Advanced</strong> and fill in \
|
||||||
<strong>Valid OAuth redirect URIs</strong> with your PlexPy URL (i.e. http://localhost:8181).<br>\
|
<strong>Valid OAuth redirect URIs</strong> with your PlexPy URL (i.e. http://localhost:8181).<br>\
|
||||||
Step 3: Fill in the <strong>PlexPy URL</strong> below with the exact same URL from Step 2.<br>\
|
Step 4: Go to <strong>App Review</strong> and toggle public to <strong>Yes</strong>.<br>\
|
||||||
Step 4: Fill in the <strong>App ID</strong> and <strong>App Secret</strong> below.<br>\
|
Step 5: Fill in the <strong>PlexPy URL</strong> below with the exact same URL from Step 3.<br>\
|
||||||
Step 5: Click the <strong>Request Authorization</strong> button below.',
|
Step 6: Fill in the <strong>App ID</strong> and <strong>App Secret</strong> below.<br>\
|
||||||
|
Step 7: Click the <strong>Request Authorization</strong> button below.<br> \
|
||||||
|
Step 8: Fill in the <strong>Group ID</strong> below.',
|
||||||
'input_type': 'help'
|
'input_type': 'help'
|
||||||
},
|
},
|
||||||
{'label': 'PlexPy URL',
|
{'label': 'PlexPy URL',
|
||||||
@@ -2125,6 +2228,18 @@ class FacebookNotifier(object):
|
|||||||
'name': 'facebook_group',
|
'name': 'facebook_group',
|
||||||
'description': 'Your Facebook Group ID.',
|
'description': 'Your Facebook Group ID.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Include Poster Image',
|
||||||
|
'value': self.incl_poster,
|
||||||
|
'name': 'facebook_incl_poster',
|
||||||
|
'description': 'Include a poster and link in the notifications.',
|
||||||
|
'input_type': 'checkbox'
|
||||||
|
},
|
||||||
|
{'label': 'Include Subject Line',
|
||||||
|
'value': self.incl_subject,
|
||||||
|
'name': 'facebook_incl_subject',
|
||||||
|
'description': 'Include the subject line in the notifications.',
|
||||||
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -86,7 +86,7 @@ def get_real_pms_url():
|
|||||||
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
||||||
if not plexpy.CONFIG.PMS_IS_REMOTE and item['local'] == '1':
|
if not plexpy.CONFIG.PMS_IS_REMOTE and item['local'] == '1' and 'plex.direct' in item['uri']:
|
||||||
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
||||||
@@ -383,7 +383,6 @@ class PlexTV(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
plextv_resources = self.get_plextv_resources(include_https=include_https)
|
plextv_resources = self.get_plextv_resources(include_https=include_https)
|
||||||
server_urls = []
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml_parse = minidom.parseString(plextv_resources)
|
xml_parse = minidom.parseString(plextv_resources)
|
||||||
@@ -400,36 +399,51 @@ class PlexTV(object):
|
|||||||
logger.warn(u"PlexPy PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
|
logger.warn(u"PlexPy PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Function to get all connections for a device
|
||||||
|
def get_connections(device):
|
||||||
|
conn = []
|
||||||
|
connections = device.getElementsByTagName('Connection')
|
||||||
|
|
||||||
|
for c in connections:
|
||||||
|
server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'),
|
||||||
|
"address": helpers.get_xml_attr(c, 'address'),
|
||||||
|
"port": helpers.get_xml_attr(c, 'port'),
|
||||||
|
"uri": helpers.get_xml_attr(c, 'uri'),
|
||||||
|
"local": helpers.get_xml_attr(c, 'local')
|
||||||
|
}
|
||||||
|
conn.append(server_details)
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
server_urls = []
|
||||||
|
|
||||||
|
# Try to match the device
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
if helpers.get_xml_attr(a, 'clientIdentifier') == server_id:
|
if helpers.get_xml_attr(a, 'clientIdentifier') == server_id:
|
||||||
connections = a.getElementsByTagName('Connection')
|
server_urls = get_connections(a)
|
||||||
for connection in connections:
|
break
|
||||||
server_details = {"protocol": helpers.get_xml_attr(connection, 'protocol'),
|
|
||||||
"address": helpers.get_xml_attr(connection, 'address'),
|
# Else no device match found
|
||||||
"port": helpers.get_xml_attr(connection, 'port'),
|
if not server_urls:
|
||||||
"uri": helpers.get_xml_attr(connection, 'uri'),
|
# Try to match the PMS_IP and PMS_PORT
|
||||||
"local": helpers.get_xml_attr(connection, 'local')
|
for a in xml_head:
|
||||||
}
|
if helpers.get_xml_attr(a, 'provides') == 'server':
|
||||||
|
connections = a.getElementsByTagName('Connection')
|
||||||
|
|
||||||
server_urls.append(server_details)
|
for connection in connections:
|
||||||
# Else try to match the PMS_IP and PMS_PORT
|
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
|
||||||
else:
|
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
|
||||||
connections = a.getElementsByTagName('Connection')
|
|
||||||
for connection in connections:
|
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
|
||||||
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
|
plexpy.CONFIG.write()
|
||||||
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
|
|
||||||
|
logger.info(u"PlexPy PlexTV :: PMS identifier changed from %s to %s." % \
|
||||||
|
(server_id, plexpy.CONFIG.PMS_IDENTIFIER))
|
||||||
|
|
||||||
|
server_urls = get_connections(a)
|
||||||
|
break
|
||||||
|
|
||||||
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
|
if server_urls:
|
||||||
|
|
||||||
logger.info(u"PlexPy PlexTV :: PMS identifier changed from %s to %s." % \
|
|
||||||
(server_id, plexpy.CONFIG.PMS_IDENTIFIER))
|
|
||||||
|
|
||||||
server_details = {"protocol": helpers.get_xml_attr(connection, 'protocol'),
|
|
||||||
"address": helpers.get_xml_attr(connection, 'address'),
|
|
||||||
"port": helpers.get_xml_attr(connection, 'port'),
|
|
||||||
"uri": helpers.get_xml_attr(connection, 'uri'),
|
|
||||||
"local": helpers.get_xml_attr(connection, 'local')
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
|
|
||||||
return server_urls
|
return server_urls
|
||||||
@@ -479,6 +493,16 @@ class PlexTV(object):
|
|||||||
connections = d.getElementsByTagName('Connection')
|
connections = d.getElementsByTagName('Connection')
|
||||||
|
|
||||||
for c in connections:
|
for c in connections:
|
||||||
|
# If this is a remote server don't show any local IPs.
|
||||||
|
if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
|
||||||
|
helpers.get_xml_attr(c, 'local') == '1':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If this is a local server don't show any remote IPs.
|
||||||
|
if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \
|
||||||
|
helpers.get_xml_attr(c, 'local') == '0':
|
||||||
|
continue
|
||||||
|
|
||||||
server = {'httpsRequired': helpers.get_xml_attr(d, 'httpsRequired'),
|
server = {'httpsRequired': helpers.get_xml_attr(d, 'httpsRequired'),
|
||||||
'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'),
|
'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'),
|
||||||
'label': helpers.get_xml_attr(d, 'name'),
|
'label': helpers.get_xml_attr(d, 'name'),
|
||||||
|
@@ -19,10 +19,11 @@ from urlparse import urlparse
|
|||||||
import plexpy
|
import plexpy
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
def get_server_friendly_name():
|
def get_server_friendly_name():
|
||||||
logger.info(u"PlexPy Pmsconnect :: Requesting name from server...")
|
logger.info(u"PlexPy Pmsconnect :: Requesting name from server...")
|
||||||
server_name = PmsConnect().get_server_pref(pref='FriendlyName')
|
server_name = PmsConnect().get_server_pref(pref='FriendlyName')
|
||||||
|
|
||||||
# If friendly name is blank
|
# If friendly name is blank
|
||||||
if not server_name:
|
if not server_name:
|
||||||
servers_info = PmsConnect().get_servers_info()
|
servers_info = PmsConnect().get_servers_info()
|
||||||
@@ -30,7 +31,7 @@ def get_server_friendly_name():
|
|||||||
if server['machine_identifier'] == plexpy.CONFIG.PMS_IDENTIFIER:
|
if server['machine_identifier'] == plexpy.CONFIG.PMS_IDENTIFIER:
|
||||||
server_name = server['name']
|
server_name = server['name']
|
||||||
break
|
break
|
||||||
|
|
||||||
if server_name and server_name != plexpy.CONFIG.PMS_NAME:
|
if server_name and server_name != plexpy.CONFIG.PMS_NAME:
|
||||||
plexpy.CONFIG.__setattr__('PMS_NAME', server_name)
|
plexpy.CONFIG.__setattr__('PMS_NAME', server_name)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
@@ -38,17 +39,22 @@ def get_server_friendly_name():
|
|||||||
|
|
||||||
return server_name
|
return server_name
|
||||||
|
|
||||||
|
|
||||||
def refresh_libraries():
|
def refresh_libraries():
|
||||||
logger.info(u"PlexPy Pmsconnect :: Requesting libraries list refresh...")
|
logger.info(u"PlexPy Pmsconnect :: Requesting libraries list refresh...")
|
||||||
library_sections = PmsConnect().get_library_details()
|
|
||||||
|
|
||||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||||
|
if not server_id:
|
||||||
|
logger.error(u"PlexPy Pmsconnect :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
|
||||||
|
return
|
||||||
|
|
||||||
library_keys = []
|
library_sections = PmsConnect().get_library_details()
|
||||||
|
|
||||||
if library_sections:
|
if library_sections:
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
library_keys = []
|
||||||
|
|
||||||
for section in library_sections:
|
for section in library_sections:
|
||||||
section_keys = {'server_id': server_id,
|
section_keys = {'server_id': server_id,
|
||||||
'section_id': section['section_id']}
|
'section_id': section['section_id']}
|
||||||
@@ -67,15 +73,15 @@ def refresh_libraries():
|
|||||||
|
|
||||||
library_keys.append(section['section_id'])
|
library_keys.append(section['section_id'])
|
||||||
|
|
||||||
|
|
||||||
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
||||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
if plexpy.CONFIG.UPDATE_SECTION_IDS == 1:
|
if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1:
|
||||||
from plexpy import libraries
|
from plexpy import libraries
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
# Start library section_id update on it's own thread
|
||||||
threading.Thread(target=libraries.update_section_ids).start()
|
threading.Thread(target=libraries.update_section_ids).start()
|
||||||
|
|
||||||
logger.info(u"PlexPy Pmsconnect :: Libraries list refreshed.")
|
logger.info(u"PlexPy Pmsconnect :: Libraries list refreshed.")
|
||||||
@@ -201,7 +207,7 @@ class PmsConnect(object):
|
|||||||
proto=self.protocol,
|
proto=self.protocol,
|
||||||
request_type='GET',
|
request_type='GET',
|
||||||
output_format=output_format)
|
output_format=output_format)
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def get_childrens_list(self, rating_key='', output_format=''):
|
def get_childrens_list(self, rating_key='', output_format=''):
|
||||||
@@ -218,7 +224,7 @@ class PmsConnect(object):
|
|||||||
proto=self.protocol,
|
proto=self.protocol,
|
||||||
request_type='GET',
|
request_type='GET',
|
||||||
output_format=output_format)
|
output_format=output_format)
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def get_server_list(self, output_format=''):
|
def get_server_list(self, output_format=''):
|
||||||
@@ -295,7 +301,7 @@ class PmsConnect(object):
|
|||||||
"""
|
"""
|
||||||
count = '&X-Plex-Container-Size=' + count if count else ''
|
count = '&X-Plex-Container-Size=' + count if count else ''
|
||||||
|
|
||||||
uri = '/library/sections/' + section_id + '/' + list_type +'?X-Plex-Container-Start=0' + count + sort_type
|
uri = '/library/sections/' + section_id + '/' + list_type + '?X-Plex-Container-Start=0' + count + sort_type
|
||||||
request = self.request_handler.make_request(uri=uri,
|
request = self.request_handler.make_request(uri=uri,
|
||||||
proto=self.protocol,
|
proto=self.protocol,
|
||||||
request_type='GET',
|
request_type='GET',
|
||||||
@@ -830,7 +836,7 @@ class PmsConnect(object):
|
|||||||
metadata = self.get_metadata_details(str(child_rating_key), get_media_info)
|
metadata = self.get_metadata_details(str(child_rating_key), get_media_info)
|
||||||
if metadata:
|
if metadata:
|
||||||
metadata_list.append(metadata['metadata'])
|
metadata_list.append(metadata['metadata'])
|
||||||
|
|
||||||
elif get_children and a.getElementsByTagName('Directory'):
|
elif get_children and a.getElementsByTagName('Directory'):
|
||||||
dir_main = a.getElementsByTagName('Directory')
|
dir_main = a.getElementsByTagName('Directory')
|
||||||
metadata_main = [d for d in dir_main if helpers.get_xml_attr(d, 'ratingKey')]
|
metadata_main = [d for d in dir_main if helpers.get_xml_attr(d, 'ratingKey')]
|
||||||
@@ -839,7 +845,7 @@ class PmsConnect(object):
|
|||||||
metadata = self.get_metadata_children_details(str(child_rating_key), get_children, get_media_info)
|
metadata = self.get_metadata_children_details(str(child_rating_key), get_children, get_media_info)
|
||||||
if metadata:
|
if metadata:
|
||||||
metadata_list.extend(metadata['metadata'])
|
metadata_list.extend(metadata['metadata'])
|
||||||
|
|
||||||
output = {'metadata': metadata_list}
|
output = {'metadata': metadata_list}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -887,7 +893,7 @@ class PmsConnect(object):
|
|||||||
metadata['section_type'] = 'track'
|
metadata['section_type'] = 'track'
|
||||||
|
|
||||||
metadata_list = {'metadata': metadata}
|
metadata_list = {'metadata': metadata}
|
||||||
|
|
||||||
return metadata_list
|
return metadata_list
|
||||||
|
|
||||||
def get_current_activity(self):
|
def get_current_activity(self):
|
||||||
@@ -990,7 +996,7 @@ class PmsConnect(object):
|
|||||||
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
||||||
|
|
||||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||||
'art': helpers.get_xml_attr(session, 'art'),
|
'art': helpers.get_xml_attr(session, 'art'),
|
||||||
@@ -1112,7 +1118,7 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
if helpers.get_xml_attr(session, 'type') == 'episode':
|
if helpers.get_xml_attr(session, 'type') == 'episode':
|
||||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||||
'art': helpers.get_xml_attr(session, 'art'),
|
'art': helpers.get_xml_attr(session, 'art'),
|
||||||
@@ -1170,7 +1176,7 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
elif helpers.get_xml_attr(session, 'type') == 'movie':
|
elif helpers.get_xml_attr(session, 'type') == 'movie':
|
||||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||||
'art': helpers.get_xml_attr(session, 'art'),
|
'art': helpers.get_xml_attr(session, 'art'),
|
||||||
@@ -1228,7 +1234,7 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
elif helpers.get_xml_attr(session, 'type') == 'clip':
|
elif helpers.get_xml_attr(session, 'type') == 'clip':
|
||||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||||
'art': helpers.get_xml_attr(session, 'art'),
|
'art': helpers.get_xml_attr(session, 'art'),
|
||||||
@@ -1319,7 +1325,7 @@ class PmsConnect(object):
|
|||||||
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
||||||
|
|
||||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||||
'art': helpers.get_xml_attr(session, 'art'),
|
'art': helpers.get_xml_attr(session, 'art'),
|
||||||
@@ -1404,7 +1410,7 @@ class PmsConnect(object):
|
|||||||
children_list = {'children_count': '0',
|
children_list = {'children_count': '0',
|
||||||
'children_list': []
|
'children_list': []
|
||||||
}
|
}
|
||||||
return parent_list
|
return children_list
|
||||||
|
|
||||||
result_data = []
|
result_data = []
|
||||||
|
|
||||||
@@ -1551,7 +1557,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(xml_head[0], 'title1'),
|
'title': helpers.get_xml_attr(xml_head[0], 'title1'),
|
||||||
'libraries_list': libraries_list
|
'libraries_list': libraries_list
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_library_children_details(self, section_id='', section_type='', list_type='all', count='', rating_key='', get_media_info=False):
|
def get_library_children_details(self, section_id='', section_type='', list_type='all', count='', rating_key='', get_media_info=False):
|
||||||
@@ -1588,9 +1594,9 @@ class PmsConnect(object):
|
|||||||
sort_type = ''
|
sort_type = ''
|
||||||
|
|
||||||
if str(section_id).isdigit():
|
if str(section_id).isdigit():
|
||||||
library_data = self.get_library_list(section_id, list_type, count, sort_type, output_format='xml')
|
library_data = self.get_library_list(str(section_id), list_type, count, sort_type, output_format='xml')
|
||||||
elif str(rating_key).isdigit():
|
elif str(rating_key).isdigit():
|
||||||
library_data = self.get_children_list(rating_key, output_format='xml')
|
library_data = self.get_children_list(str(rating_key), output_format='xml')
|
||||||
else:
|
else:
|
||||||
logger.warn(u"PlexPy Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
|
logger.warn(u"PlexPy Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
|
||||||
return []
|
return []
|
||||||
@@ -1608,15 +1614,15 @@ class PmsConnect(object):
|
|||||||
if a.getAttribute('size') == '0':
|
if a.getAttribute('size') == '0':
|
||||||
logger.debug(u"PlexPy Pmsconnect :: No library data.")
|
logger.debug(u"PlexPy Pmsconnect :: No library data.")
|
||||||
childern_list = {'library_count': '0',
|
childern_list = {'library_count': '0',
|
||||||
'childern_list': []
|
'childern_list': []
|
||||||
}
|
}
|
||||||
return childern_list
|
return childern_list
|
||||||
|
|
||||||
if rating_key:
|
if rating_key:
|
||||||
library_count = helpers.get_xml_attr(xml_head[0], 'size')
|
library_count = helpers.get_xml_attr(xml_head[0], 'size')
|
||||||
else:
|
else:
|
||||||
library_count = helpers.get_xml_attr(xml_head[0], 'totalSize')
|
library_count = helpers.get_xml_attr(xml_head[0], 'totalSize')
|
||||||
|
|
||||||
# Get show/season info from xml_head
|
# Get show/season info from xml_head
|
||||||
|
|
||||||
item_main = []
|
item_main = []
|
||||||
@@ -1668,7 +1674,7 @@ class PmsConnect(object):
|
|||||||
output = {'library_count': library_count,
|
output = {'library_count': library_count,
|
||||||
'childern_list': childern_list
|
'childern_list': childern_list
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_library_details(self):
|
def get_library_details(self):
|
||||||
@@ -1689,8 +1695,8 @@ class PmsConnect(object):
|
|||||||
section_id = library['section_id']
|
section_id = library['section_id']
|
||||||
children_list = self.get_library_children_details(section_id=section_id, section_type=section_type, count='1')
|
children_list = self.get_library_children_details(section_id=section_id, section_type=section_type, count='1')
|
||||||
|
|
||||||
if children_list and children_list['library_count'] != '0':
|
if children_list:
|
||||||
library_stats = {'section_id': library['section_id'],
|
library_stats = {'section_id': section_id,
|
||||||
'section_name': library['section_name'],
|
'section_name': library['section_name'],
|
||||||
'section_type': section_type,
|
'section_type': section_type,
|
||||||
'thumb': library['thumb'],
|
'thumb': library['thumb'],
|
||||||
@@ -1783,7 +1789,7 @@ class PmsConnect(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_search_result_details: %s." % e)
|
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_search_result_details: %s." % e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
search_results_count = 0
|
search_results_count = 0
|
||||||
search_results_list = {'movie': [],
|
search_results_list = {'movie': [],
|
||||||
'show': [],
|
'show': [],
|
||||||
@@ -1801,8 +1807,8 @@ class PmsConnect(object):
|
|||||||
if totalSize == 0:
|
if totalSize == 0:
|
||||||
logger.debug(u"PlexPy Pmsconnect :: No search results.")
|
logger.debug(u"PlexPy Pmsconnect :: No search results.")
|
||||||
search_results_list = {'results_count': search_results_count,
|
search_results_list = {'results_count': search_results_count,
|
||||||
'results_list': []
|
'results_list': []
|
||||||
}
|
}
|
||||||
return search_results_list
|
return search_results_list
|
||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
@@ -1907,7 +1913,7 @@ class PmsConnect(object):
|
|||||||
if a.getAttribute('size'):
|
if a.getAttribute('size'):
|
||||||
if a.getAttribute('size') == '0':
|
if a.getAttribute('size') == '0':
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
title = helpers.get_xml_attr(a, 'title2')
|
title = helpers.get_xml_attr(a, 'title2')
|
||||||
|
|
||||||
if a.getElementsByTagName('Directory'):
|
if a.getElementsByTagName('Directory'):
|
||||||
@@ -1952,34 +1958,33 @@ class PmsConnect(object):
|
|||||||
if child_rating_key:
|
if child_rating_key:
|
||||||
key = int(child_index)
|
key = int(child_index)
|
||||||
children.update({key: {'rating_key': int(child_rating_key)}})
|
children.update({key: {'rating_key': int(child_rating_key)}})
|
||||||
|
|
||||||
key = int(parent_index) if match_type == 'index' else parent_title
|
key = int(parent_index) if match_type == 'index' else parent_title
|
||||||
parents.update({key:
|
parents.update({key:
|
||||||
{'rating_key': int(parent_rating_key),
|
{'rating_key': int(parent_rating_key),
|
||||||
'children': children}
|
'children': children}
|
||||||
})
|
})
|
||||||
|
|
||||||
key = 0 if match_type == 'index' else title
|
key = 0 if match_type == 'index' else title
|
||||||
key_list = {key:
|
key_list = {key: {'rating_key': int(rating_key),
|
||||||
{'rating_key': int(rating_key),
|
'children': parents},
|
||||||
'children': parents },
|
'section_id': section_id,
|
||||||
'section_id': section_id,
|
'library_name': library_name
|
||||||
'library_name': library_name
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return key_list
|
return key_list
|
||||||
|
|
||||||
def get_server_response(self):
|
def get_server_response(self):
|
||||||
# Refresh Plex remote access port mapping first
|
# Refresh Plex remote access port mapping first
|
||||||
self.put_refresh_reachability()
|
self.put_refresh_reachability()
|
||||||
account_data = self.get_account(output_format='xml')
|
account_data = self.get_account(output_format='xml')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml_head = account_data.getElementsByTagName('MyPlex')
|
xml_head = account_data.getElementsByTagName('MyPlex')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_server_response: %s." % e)
|
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_server_response: %s." % e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
server_response = {}
|
server_response = {}
|
||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
@@ -1988,5 +1993,5 @@ class PmsConnect(object):
|
|||||||
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
|
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
|
||||||
'public_port': helpers.get_xml_attr(a, 'publicPort')
|
'public_port': helpers.get_xml_attr(a, 'publicPort')
|
||||||
}
|
}
|
||||||
|
|
||||||
return server_response
|
return server_response
|
||||||
|
100
plexpy/users.py
100
plexpy/users.py
@@ -32,6 +32,9 @@ class Users(object):
|
|||||||
'users.thumb AS user_thumb',
|
'users.thumb AS user_thumb',
|
||||||
'users.custom_avatar_url AS custom_thumb',
|
'users.custom_avatar_url AS custom_thumb',
|
||||||
'COUNT(session_history.id) AS plays',
|
'COUNT(session_history.id) AS plays',
|
||||||
|
'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \
|
||||||
|
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
|
||||||
|
session_history.paused_counter END) AS duration',
|
||||||
'MAX(session_history.started) AS last_seen',
|
'MAX(session_history.started) AS last_seen',
|
||||||
'MAX(session_history.id) AS id',
|
'MAX(session_history.id) AS id',
|
||||||
'session_history_metadata.full_title AS last_played',
|
'session_history_metadata.full_title AS last_played',
|
||||||
@@ -100,6 +103,7 @@ class Users(object):
|
|||||||
'friendly_name': item['friendly_name'],
|
'friendly_name': item['friendly_name'],
|
||||||
'user_thumb': user_thumb,
|
'user_thumb': user_thumb,
|
||||||
'plays': item['plays'],
|
'plays': item['plays'],
|
||||||
|
'duration': item['duration'],
|
||||||
'last_seen': item['last_seen'],
|
'last_seen': item['last_seen'],
|
||||||
'last_played': item['last_played'],
|
'last_played': item['last_played'],
|
||||||
'id': item['id'],
|
'id': item['id'],
|
||||||
@@ -241,61 +245,26 @@ class Users(object):
|
|||||||
def get_details(self, user_id=None, user=None):
|
def get_details(self, user_id=None, user=None):
|
||||||
from plexpy import plextv
|
from plexpy import plextv
|
||||||
|
|
||||||
monitor_db = database.MonitorDatabase()
|
default_return = {'user_id': None,
|
||||||
|
'username': 'Local',
|
||||||
try:
|
'friendly_name': 'Local',
|
||||||
if str(user_id).isdigit():
|
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
'email': '',
|
||||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
'is_home_user': 0,
|
||||||
'FROM users ' \
|
'is_allow_sync': 0,
|
||||||
'WHERE user_id = ? '
|
'is_restricted': 0,
|
||||||
result = monitor_db.select(query, args=[user_id])
|
'do_notify': 0,
|
||||||
elif user:
|
'keep_history': 0
|
||||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
}
|
||||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
|
||||||
'FROM users ' \
|
|
||||||
'WHERE username = ? '
|
|
||||||
result = monitor_db.select(query, args=[user])
|
|
||||||
else:
|
|
||||||
result = []
|
|
||||||
except Exception as e:
|
|
||||||
logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e)
|
|
||||||
result = []
|
|
||||||
|
|
||||||
if result:
|
if not user_id and not user:
|
||||||
user_details = {}
|
return default_return
|
||||||
for item in result:
|
|
||||||
if item['friendly_name']:
|
|
||||||
friendly_name = item['friendly_name']
|
|
||||||
else:
|
|
||||||
friendly_name = item['username']
|
|
||||||
|
|
||||||
if item['custom_thumb'] and item['custom_thumb'] != item['user_thumb']:
|
def get_user_details(user_id=user_id, user=user):
|
||||||
user_thumb = item['custom_thumb']
|
monitor_db = database.MonitorDatabase()
|
||||||
elif item['user_thumb']:
|
|
||||||
user_thumb = item['user_thumb']
|
|
||||||
else:
|
|
||||||
user_thumb = common.DEFAULT_USER_THUMB
|
|
||||||
|
|
||||||
user_details = {'user_id': item['user_id'],
|
|
||||||
'username': item['username'],
|
|
||||||
'friendly_name': friendly_name,
|
|
||||||
'user_thumb': user_thumb,
|
|
||||||
'email': item['email'],
|
|
||||||
'is_home_user': item['is_home_user'],
|
|
||||||
'is_allow_sync': item['is_allow_sync'],
|
|
||||||
'is_restricted': item['is_restricted'],
|
|
||||||
'do_notify': item['do_notify'],
|
|
||||||
'keep_history': item['keep_history']
|
|
||||||
}
|
|
||||||
return user_details
|
|
||||||
else:
|
|
||||||
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Requesting user list refresh.")
|
|
||||||
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
|
|
||||||
try:
|
try:
|
||||||
if str(user_id).isdigit():
|
if str(user_id).isdigit():
|
||||||
# Refresh users
|
|
||||||
plextv.refresh_users()
|
|
||||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
||||||
'FROM users ' \
|
'FROM users ' \
|
||||||
@@ -313,8 +282,8 @@ class Users(object):
|
|||||||
logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e)
|
logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e)
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
|
user_details = {}
|
||||||
if result:
|
if result:
|
||||||
user_details = {}
|
|
||||||
for item in result:
|
for item in result:
|
||||||
if item['friendly_name']:
|
if item['friendly_name']:
|
||||||
friendly_name = item['friendly_name']
|
friendly_name = item['friendly_name']
|
||||||
@@ -339,21 +308,28 @@ class Users(object):
|
|||||||
'do_notify': item['do_notify'],
|
'do_notify': item['do_notify'],
|
||||||
'keep_history': item['keep_history']
|
'keep_history': item['keep_history']
|
||||||
}
|
}
|
||||||
|
return user_details
|
||||||
|
|
||||||
|
user_details = get_user_details(user_id=user_id, user=user)
|
||||||
|
|
||||||
|
if user_details:
|
||||||
|
return user_details
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Requesting user list refresh.")
|
||||||
|
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
|
||||||
|
plextv.refresh_users()
|
||||||
|
|
||||||
|
user_details = get_user_details(user_id=user_id, user=user)
|
||||||
|
|
||||||
|
if user_details:
|
||||||
return user_details
|
return user_details
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Returning 'Local' user.")
|
||||||
# If there is no user data we must return something
|
# If there is no user data we must return something
|
||||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||||
return {'user_id': None,
|
return default_return
|
||||||
'username': 'Local',
|
|
||||||
'friendly_name': 'Local',
|
|
||||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
|
||||||
'email': '',
|
|
||||||
'is_home_user': 0,
|
|
||||||
'is_allow_sync': 0,
|
|
||||||
'is_restricted': 0,
|
|
||||||
'do_notify': 0,
|
|
||||||
'keep_history': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_watch_time_stats(self, user_id=None):
|
def get_watch_time_stats(self, user_id=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_VERSION = "master"
|
PLEXPY_VERSION = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "1.3.1"
|
PLEXPY_RELEASE_VERSION = "1.3.9"
|
||||||
|
@@ -37,10 +37,15 @@ def start_thread():
|
|||||||
def run():
|
def run():
|
||||||
from websocket import create_connection
|
from websocket import create_connection
|
||||||
|
|
||||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
||||||
plexpy.CONFIG.PMS_IP,
|
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||||
plexpy.CONFIG.PMS_PORT
|
secure = ' secure'
|
||||||
)
|
else:
|
||||||
|
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||||
|
plexpy.CONFIG.PMS_IP,
|
||||||
|
plexpy.CONFIG.PMS_PORT
|
||||||
|
)
|
||||||
|
secure = ''
|
||||||
|
|
||||||
# Set authentication token (if one is available)
|
# Set authentication token (if one is available)
|
||||||
if plexpy.CONFIG.PMS_TOKEN:
|
if plexpy.CONFIG.PMS_TOKEN:
|
||||||
@@ -52,7 +57,7 @@ def run():
|
|||||||
# Try an open the websocket connection - if it fails after 15 retries fallback to polling
|
# Try an open the websocket connection - if it fails after 15 retries fallback to polling
|
||||||
while not ws_connected and reconnects <= 15:
|
while not ws_connected and reconnects <= 15:
|
||||||
try:
|
try:
|
||||||
logger.info(u'PlexPy WebSocket :: Opening websocket, connection attempt %s.' % str(reconnects + 1))
|
logger.info(u'PlexPy WebSocket :: Opening%s websocket, connection attempt %s.' % (secure, str(reconnects + 1)))
|
||||||
ws = create_connection(uri)
|
ws = create_connection(uri)
|
||||||
reconnects = 0
|
reconnects = 0
|
||||||
ws_connected = True
|
ws_connected = True
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,13 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import cherrypy
|
|
||||||
import plexpy
|
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
from plexpy import logger
|
from plexpy import logger
|
||||||
from plexpy.webserve import WebInterface
|
import plexpy
|
||||||
from plexpy.helpers import create_https_certificates
|
from plexpy.helpers import create_https_certificates
|
||||||
|
from plexpy.webserve import WebInterface
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(options):
|
def initialize(options):
|
||||||
@@ -31,17 +32,15 @@ def initialize(options):
|
|||||||
https_key = options['https_key']
|
https_key = options['https_key']
|
||||||
|
|
||||||
if enable_https:
|
if enable_https:
|
||||||
# If either the HTTPS certificate or key do not exist, try to make
|
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
||||||
# self-signed ones.
|
if plexpy.CONFIG.HTTPS_CREATE_CERT and \
|
||||||
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
|
(not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key))):
|
||||||
if not create_https_certificates(https_cert, https_key):
|
if not create_https_certificates(https_cert, https_key):
|
||||||
logger.warn("Unable to create certificate and key. Disabling " \
|
logger.warn("Unable to create certificate and key. Disabling HTTPS")
|
||||||
"HTTPS")
|
|
||||||
enable_https = False
|
enable_https = False
|
||||||
|
|
||||||
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
||||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
logger.warn("Disabled HTTPS because of missing certificate and key.")
|
||||||
"key.")
|
|
||||||
enable_https = False
|
enable_https = False
|
||||||
|
|
||||||
options_dict = {
|
options_dict = {
|
||||||
@@ -63,13 +62,17 @@ def initialize(options):
|
|||||||
protocol = "http"
|
protocol = "http"
|
||||||
|
|
||||||
logger.info("Starting PlexPy web server on %s://%s:%d/", protocol,
|
logger.info("Starting PlexPy web server on %s://%s:%d/", protocol,
|
||||||
options['http_host'], options['http_port'])
|
options['http_host'], options['http_port'])
|
||||||
cherrypy.config.update(options_dict)
|
cherrypy.config.update(options_dict)
|
||||||
|
|
||||||
conf = {
|
conf = {
|
||||||
'/': {
|
'/': {
|
||||||
'tools.staticdir.root': os.path.join(plexpy.PROG_DIR, 'data'),
|
'tools.staticdir.root': os.path.join(plexpy.PROG_DIR, 'data'),
|
||||||
'tools.proxy.on': options['http_proxy'] # pay attention to X-Forwarded-Proto header
|
'tools.proxy.on': options['http_proxy'], # pay attention to X-Forwarded-Proto header
|
||||||
|
'tools.gzip.on': True,
|
||||||
|
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
||||||
|
'text/javascript', 'application/json',
|
||||||
|
'application/javascript']
|
||||||
},
|
},
|
||||||
'/interfaces': {
|
'/interfaces': {
|
||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
@@ -87,15 +90,15 @@ def initialize(options):
|
|||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
'tools.staticdir.dir': "js"
|
'tools.staticdir.dir': "js"
|
||||||
},
|
},
|
||||||
'/favicon.ico': {
|
|
||||||
'tools.staticfile.on': True,
|
|
||||||
'tools.staticfile.filename': os.path.join(os.path.abspath(
|
|
||||||
os.curdir), "images" + os.sep + "favicon.ico")
|
|
||||||
},
|
|
||||||
'/cache': {
|
'/cache': {
|
||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
'tools.staticdir.dir': plexpy.CONFIG.CACHE_DIR
|
'tools.staticdir.dir': plexpy.CONFIG.CACHE_DIR
|
||||||
|
},
|
||||||
|
'/favicon.ico': {
|
||||||
|
'tools.staticfile.on': True,
|
||||||
|
'tools.staticfile.filename': os.path.abspath(os.path.join(plexpy.PROG_DIR, 'data/interfaces/default/images/favicon.ico'))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if options['http_password']:
|
if options['http_password']:
|
||||||
|
Reference in New Issue
Block a user