Compare commits
172 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
08537c1d69 | ||
![]() |
425da82f5f | ||
![]() |
2cfbf7c39a | ||
![]() |
d4eed9f8fd | ||
![]() |
fe10170826 | ||
![]() |
8dc3b0b250 | ||
![]() |
75da1220af | ||
![]() |
37a2c3c631 | ||
![]() |
5c5722714d | ||
![]() |
2ba529f9e3 | ||
![]() |
fd760ff015 | ||
![]() |
daab1d917b | ||
![]() |
4c3a63a7e1 | ||
![]() |
7cc58b84da | ||
![]() |
2bac4ac1a7 | ||
![]() |
c5b2b86786 | ||
![]() |
5652a2b6c2 | ||
![]() |
bd19f543a2 | ||
![]() |
cc1e888227 | ||
![]() |
d73e379dcf | ||
![]() |
0569abd00d | ||
![]() |
7f5d9bec87 | ||
![]() |
b39e7bbb6d | ||
![]() |
70270a8e3b | ||
![]() |
efdc050a28 | ||
![]() |
e8a65df7f0 | ||
![]() |
59628a72fb | ||
![]() |
a4d6c6c0d8 | ||
![]() |
bea82c6640 | ||
![]() |
1ba3bdfbda | ||
![]() |
98b4000bc0 | ||
![]() |
14f6824931 | ||
![]() |
795d7d0a93 | ||
![]() |
673fa2b556 | ||
![]() |
0e2504fc78 | ||
![]() |
2afca9f2b4 | ||
![]() |
464d2a541d | ||
![]() |
fa8c5e0982 | ||
![]() |
5e15884d8f | ||
![]() |
b5e9ff3b4e | ||
![]() |
fed7d4cc34 | ||
![]() |
d7ab066ff8 | ||
![]() |
4100917016 | ||
![]() |
5d2c1ffb88 | ||
![]() |
ddb0f198a9 | ||
![]() |
13438e3e25 | ||
![]() |
05a410b327 | ||
![]() |
23fa64d289 | ||
![]() |
1920c9b7e3 | ||
![]() |
eedd0d9c07 | ||
![]() |
9ef389d335 | ||
![]() |
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
|
||||
*.pem
|
||||
|
||||
# Mergetool
|
||||
*.orgin
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store?
|
||||
@@ -32,7 +35,7 @@ Icon?
|
||||
Thumbs.db
|
||||
|
||||
#Ignore files generated by PyCharm
|
||||
.idea/*
|
||||
*.idea/*
|
||||
|
||||
#Ignore files generated by vi
|
||||
*.swp
|
||||
|
158
CHANGELOG.md
158
CHANGELOG.md
@@ -1,5 +1,129 @@
|
||||
# Changelog
|
||||
|
||||
## v1.3.10 (2016-03-12)
|
||||
|
||||
* Fix: Actually allow HTML tags for Pushover.
|
||||
* Fix: PlexPy not restarting on Windows if there is a space in the folder path.
|
||||
* Fix: Reconnect websocket when changing PMS SSL setting.
|
||||
* Fix: Datatables not loading when view_offset or duration is blank.
|
||||
* Fix: Bug when checking the PMS version in the settings.
|
||||
* Fix: Auto-refreshing of log tables.
|
||||
* Fix: Logging of IPv6 addresses. (PMS version >0.9.14 only.)
|
||||
* Fix: Hide days selection from the Play Totals graph page.
|
||||
* Fix: PlexPy overwriting user's own SSL certificate/key.
|
||||
* Fix: Multiple watched notifications when using websocket.
|
||||
* Fix: Some missing python library imports.
|
||||
* Fix: Some typos in settings and PlexWatch importer.
|
||||
* New: Ability to get notified of PMS updates.
|
||||
* New: Ability to disable the link to Plex Web with Facebook notifications and use IMDB, TVDB, TMDb, or Last.fm instead.
|
||||
* New: Ability to reset Imgur poster url from the info page if the poster is changed.
|
||||
* New: Tooltips on the current activity progress bars.
|
||||
* New: Side scrolling of Recently Added/Recently Played items.
|
||||
* New: Document all date/time format options.
|
||||
* New: Button to clear notification logs.
|
||||
* New: Customizable backup, cache, and log directories.
|
||||
* Change: Retry writing sessions to history if it fails, so sessions don't get lost. (Activity pinger only, not availble for websocket.)
|
||||
* Change: Save any unknown sessions to the "Local" user.
|
||||
* Change: History table modal is filtered depending on which graph series is clicked.
|
||||
* Change: Revert back to saving the state of datatables (search, sorting, entries per page, etc.).
|
||||
* Change: Newlines are not longer stripped from notification text which allows for finer control of how notifications look.
|
||||
* Change: Updated FreeNAS/FreeBSD init scripts. (Must have updated jails.) (Thanks @chiviak)
|
||||
|
||||
|
||||
## v1.3.9 (2016-02-21)
|
||||
|
||||
* Fix: Recently added notification not sent to all notification agents.
|
||||
* New: 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.
|
||||
* New: Notification Logs table in the Logs tab.
|
||||
* New: 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.
|
||||
* New: ETA to Current Activity overlay.
|
||||
* New: Total duration to Libraries and Users tables.
|
||||
* New: {machine_id} to notification options.
|
||||
* New: IMDB, TVDB, TMDb, Last.fm, and Trackt IDs/URLs to notification options.
|
||||
* New: {poster_url} to notification options using Imgur.
|
||||
* New: Poster and link for Facebook notifications.
|
||||
* New: Log javascript errors from the Web UI.
|
||||
* New: Configuration and Scheduler info to the settings page.
|
||||
* New: Schedule background task to backup the PlexPy database.
|
||||
* New: URL anonymizer for external links.
|
||||
* New: Plex Media Scanner log file to Log viewer.
|
||||
* New: 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.
|
||||
* New: {stream_time}, {remaining_time}, and {progress_time} to notification options.
|
||||
* New: Powershell script support. (Thanks @Hellowlol)
|
||||
* New: 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.
|
||||
* New: 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)
|
||||
|
||||
* Fix: Notifiers authorization popups for reverse proxies.
|
||||
@@ -7,27 +131,27 @@
|
||||
* Fix: Star rating overlapping text.
|
||||
* Fix: Unable to startup when library refresh fails.
|
||||
* Fix: Unable to parse 'datestamp' and 'timestamp' format.
|
||||
* Change: Rename "Last Watched" to "Last Played"
|
||||
* Change: More descriptive libraries updating message
|
||||
* Change: Rename "Last Watched" to "Last Played".
|
||||
* Change: More descriptive libraries updating message.
|
||||
|
||||
|
||||
## v1.3.0 (2016-01-23)
|
||||
|
||||
* Add: Brand new Libraries section.
|
||||
* Add: Lots of new library statistics.
|
||||
* Add: Media info table for libraries.
|
||||
* Add: Web app for Android and iOS. (Thanks @zobe123)
|
||||
* Add: Slack notification agent. (Thanks @richipargo)
|
||||
* Add: Facebook notification agent.
|
||||
* Add: Custom script notification agent. (Thanks @Hellowlol)
|
||||
* Add: Custom "From Name" to email notification agent.
|
||||
* Add: Ability to test notifications / send custom one-off notifications.
|
||||
* Add: 'datestamp' and 'timestamp' notification options.
|
||||
* Add: More concurrent stream statistics.
|
||||
* Add: Media info flags on the info pages.
|
||||
* Add: Ability to fix broken metadata if the item has been moved in Plex.
|
||||
* Add: Ability to rearrange the homepage statistics cards.
|
||||
* Add: CentOS startup script (Thanks @PHoSawyer)
|
||||
* New: Brand new Libraries section.
|
||||
* New: Lots of new library statistics.
|
||||
* New: Media info table for libraries.
|
||||
* New: Web app for Android and iOS. (Thanks @zobe123)
|
||||
* New: Slack notification agent. (Thanks @richipargo)
|
||||
* New: Facebook notification agent.
|
||||
* New: Custom script notification agent. (Thanks @Hellowlol)
|
||||
* New: Custom "From Name" to email notification agent.
|
||||
* New: Ability to test notifications / send custom one-off notifications.
|
||||
* New: 'datestamp' and 'timestamp' notification options.
|
||||
* New: More concurrent stream statistics.
|
||||
* New: Media info flags on the info pages.
|
||||
* New: Ability to fix broken metadata if the item has been moved in Plex.
|
||||
* New: Ability to rearrange the homepage statistics cards.
|
||||
* New: CentOS startup script (Thanks @PHoSawyer)
|
||||
* Fix: Server name blank after first run wizard.
|
||||
* Fix: Incorrect duration for grouped home stats.
|
||||
* Fix: Allow SSL when verifying server in settings.
|
||||
|
@@ -3,10 +3,43 @@
|
||||
## 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.
|
||||
|
||||
* 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.
|
||||
* Visit the [Troubleshooting](../../wiki/TroubleShooting) wiki first.
|
||||
* Use [proper formatting](https://help.github.com/articles/github-flavored-markdown/). Paste your logs in code blocks.
|
||||
* Close your issue if you resolved it.
|
||||
##### 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.
|
||||
|
||||
## Pull Requests
|
||||
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
|
||||
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db')
|
||||
|
||||
# Read config and start logging
|
||||
plexpy.initialize(config_file)
|
||||
|
||||
if plexpy.DAEMON:
|
||||
plexpy.daemonize()
|
||||
|
||||
# Read config and start logging
|
||||
plexpy.initialize(config_file)
|
||||
|
||||
# Force the http port if neccessary
|
||||
if 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)
|
||||
|
||||
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
|
||||
-----------
|
||||
* PlexPy Wiki: https://github.com/drzoidberg33/plexpy/wiki
|
||||
## Installation and Support
|
||||
|
||||
|
||||
###Features
|
||||
-----------
|
||||
* 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).
|
||||
* [Installation Guides](https://github.com/drzoidberg33/plexpy/wiki/Installation) shows you how to install PlexPy.
|
||||
* [FAQs](https://github.com/drzoidberg33/plexpy/wiki/Frequently-Asked-Questions-(FAQ)) in the wiki can help you with common problems.
|
||||
|
||||
**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
|
||||
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.
|
@@ -1,6 +1,7 @@
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy import version
|
||||
from plexpy.helpers import anon_url
|
||||
%>
|
||||
<!doctype html>
|
||||
|
||||
@@ -136,15 +137,15 @@ from plexpy import version
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
||||
<div id="updatebar" style="display: none;">
|
||||
You're running an unknown version of PlexPy. <a href="update">Update</a> or
|
||||
<a href="#" id="updateDismiss">Close</a>
|
||||
You're running an unknown version of PlexPy.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
|
||||
</div>
|
||||
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
||||
<div id="updatebar" style="display: none;">
|
||||
A <a
|
||||
href="https://github.com/${plexpy.CONFIG.GIT_USER}/plexpy/compare/${plexpy.CURRENT_VERSION}...${plexpy.LATEST_VERSION}" target="_blank">
|
||||
newer version</a> is available. You're ${plexpy.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or
|
||||
<a href="#" id="updateDismiss">Close</a>
|
||||
A <a href="${anon_url('https://github.com/%s/plexpy/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
||||
newer version</a> is available.<br />
|
||||
You're ${plexpy.COMMITS_BEHIND} commits behind.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
|
||||
</div>
|
||||
% endif
|
||||
<nav class="navbar navbar-fixed-top">
|
||||
|
@@ -501,7 +501,8 @@ textarea.form-control:focus {
|
||||
.libraries-poster-face {
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
background-size: contain;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
@@ -881,10 +882,11 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
||||
}
|
||||
.dashboard-recent-media-row {
|
||||
width: 100%;
|
||||
margin:0 auto;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dashboard-recent-media {
|
||||
width: 100%;
|
||||
@@ -1717,7 +1719,8 @@ a:hover .item-children-poster {
|
||||
float: left;
|
||||
margin-top: 15px;
|
||||
margin-right: 15px;
|
||||
background-size: contain;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
@@ -2178,6 +2181,10 @@ a .home-platforms-instance-list-oval:hover,
|
||||
.refresh-libraries-button {
|
||||
float: right;
|
||||
}
|
||||
.refresh-users-button,
|
||||
.refresh-libraries-button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.nav-settings,
|
||||
.nav-settings ul {
|
||||
margin: 0px 0px 20px 0px;
|
||||
@@ -2401,6 +2408,9 @@ a .home-platforms-instance-list-oval:hover,
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.history-thumbnail-popover.popover.left {
|
||||
margin-left: -15px;
|
||||
}
|
||||
.history-thumbnail-popover.popover.right {
|
||||
margin-left: 15px;
|
||||
}
|
||||
@@ -2649,6 +2659,7 @@ table[id^='media_info_child'] table[id^='media_info_child'] thead th {
|
||||
margin: 5px 0 0 0.5em;
|
||||
}
|
||||
.notification-params {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
background-color: #282828;
|
||||
}
|
||||
@@ -2663,6 +2674,14 @@ table[id^='media_info_child'] table[id^='media_info_child'] thead th {
|
||||
padding-left: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
.notification-params.time-options td:first-child {
|
||||
padding-left: 10px;
|
||||
width: 125px;
|
||||
}
|
||||
.notification-params.time-options td:nth-child(2) {
|
||||
padding-left: 10px;
|
||||
width: 275px;
|
||||
}
|
||||
.notification-params td:not(:first-child) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -2712,4 +2731,62 @@ table[id^='media_info_child'] table[id^='media_info_child'] thead th {
|
||||
}
|
||||
.selectize-input input[type='text'] {
|
||||
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;
|
||||
}
|
||||
.nav-dashboard > li {
|
||||
float: left;
|
||||
}
|
||||
.btn-gray.disabled,
|
||||
.btn-gray.disabled:focus,
|
||||
.btn-gray.disabled:hover,
|
||||
.btn-gray.disabled:active {
|
||||
color: #323232;
|
||||
cursor: default;
|
||||
}
|
||||
.nav-header > li > a:focus,
|
||||
.nav-header > li > a:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
#recently-added-row-scroller,
|
||||
#recently-watched-row-scroller {
|
||||
position: relative;
|
||||
}
|
@@ -198,6 +198,13 @@ DOCUMENTATION :: END
|
||||
% else:
|
||||
<span>IP: N/A</span>
|
||||
% 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 class="dashboard-activity-poster-info-time">
|
||||
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
|
||||
@@ -210,8 +217,8 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
<div class="dashboard-activity-progress">
|
||||
<div class="dashboard-activity-progress-bar">
|
||||
<div class="bufferbar" style="width: ${a['transcode_progress']}%">${a['transcode_progress']}%</div>
|
||||
<div class="bar" style="width: ${a['progress_percent']}%">${a['progress_percent']}%</div>
|
||||
<div class="bufferbar" style="width: ${a['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress">${a['transcode_progress']}%</div>
|
||||
<div class="bar" style="width: ${a['progress_percent']}%" data-toggle="tooltip" title="Stream Progress">${a['progress_percent']}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-wrapper">
|
||||
@@ -254,11 +261,7 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-user">
|
||||
% if a['user_id']:
|
||||
<a href="user?user_id=${a['user_id']}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||
% else:
|
||||
<a href="user?user=${a['user']}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,11 +285,13 @@ DOCUMENTATION :: END
|
||||
});
|
||||
|
||||
// Add hover class to dashboard-instance
|
||||
$('.dashboard-activity-poster').hover(function() {
|
||||
$('.dashboard-activity-poster, .dashboard-activity-progress-bar').hover(function() {
|
||||
$(this).closest('.dashboard-instance').addClass('hover');
|
||||
}, function() {
|
||||
$(this).closest('.dashboard-instance').removeClass('hover');
|
||||
});
|
||||
|
||||
$('.bar, .bufferbar').tooltip({container: 'body', placement: 'right', delay: 50});
|
||||
</script>
|
||||
% else:
|
||||
<div class="text-muted">Nothing is currently being watched.</div><br>
|
||||
|
@@ -249,7 +249,7 @@
|
||||
|
||||
<script>
|
||||
// Modal popup dialog
|
||||
function selectHandler(selectedDate) {
|
||||
function selectHandler(selectedDate, selectedSeries) {
|
||||
|
||||
try
|
||||
{
|
||||
@@ -259,10 +259,25 @@
|
||||
var y = dateValue.getFullYear();
|
||||
var dateString = '' + y + '-' + (m<=9 ? '0' + m : m) + '-' + (d <= 9 ? '0' + d : d);
|
||||
|
||||
var media_type = 'all';
|
||||
var transcode_decision = null;
|
||||
switch(selectedSeries) {
|
||||
case "TV": media_type = 'episode'; break;
|
||||
case "Movies": media_type = 'movie'; break;
|
||||
case "Music": media_type = 'track'; break;
|
||||
case "Direct Play": transcode_decision = 'direct play'; break;
|
||||
case "Direct Stream": transcode_decision = 'copy'; break;
|
||||
case "Transcode": transcode_decision = 'transcode'; break;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
"url": "history_table_modal",
|
||||
url: "history_table_modal",
|
||||
type: 'post',
|
||||
data: { 'start_date': dateString },
|
||||
data: {
|
||||
start_date: dateString,
|
||||
media_type: media_type,
|
||||
transcode_decision: transcode_decision
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$('#history-modal').modal('show');
|
||||
$("#history-modal").html(xhr.responseText);
|
||||
@@ -271,7 +286,7 @@
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
console.log("Failed to retrieve data");
|
||||
console.log("Failed to retrieve history modal data.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -299,6 +314,8 @@
|
||||
var music_visible = (${config['music_logging_enable']} == 1 ? true : false);
|
||||
|
||||
function loadGraphsTab1(time_range, yaxis) {
|
||||
$('#days-selection').show();
|
||||
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
$.ajax({
|
||||
@@ -382,6 +399,8 @@
|
||||
}
|
||||
|
||||
function loadGraphsTab2(time_range, yaxis) {
|
||||
$('#days-selection').show();
|
||||
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
$.ajax({
|
||||
@@ -460,6 +479,8 @@
|
||||
}
|
||||
|
||||
function loadGraphsTab3(yaxis) {
|
||||
$('#days-selection').hide();
|
||||
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
$.ajax({
|
||||
|
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<table class="display history_table" id="history_table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="delete_row">Delete</th>
|
||||
|
@@ -5,12 +5,12 @@
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||
<h4 class="modal-title" id="myModalLabel">
|
||||
<strong><span id="modal_header_ip_address">
|
||||
<i class="fa fa-history"></i> History for <span id="date-header">${data}</span>
|
||||
<i class="fa fa-history"></i> History for <span id="date-header">${data['start_date']}</span>
|
||||
</span></strong>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-text">
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<table class="display history_table" id="history_table_modal" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="started">Started</th>
|
||||
@@ -32,7 +32,7 @@
|
||||
<script src="interfaces/default/js/tables/history_table_modal.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#date-header').html(moment('${data}','YYYY-MM-DD').format('ddd MMM Do YYYY'));
|
||||
$('#date-header').html(moment('${data["start_date"]}','YYYY-MM-DD').format('ddd MMM Do YYYY'));
|
||||
history_table_modal_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
@@ -40,14 +40,16 @@
|
||||
return {
|
||||
json_data: JSON.stringify(d),
|
||||
grouping: false,
|
||||
start_date: '${data}'
|
||||
};
|
||||
start_date: "${data['start_date']}",
|
||||
media_type: "${data.get('media_type')}",
|
||||
transcode_decision: "${data.get('transcode_decision')}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
history_table = $('#history_table').DataTable(history_table_modal_options);
|
||||
history_table = $('#history_table_modal').DataTable(history_table_modal_options);
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
clearSearchButton('history_table_modal', history_table);
|
||||
|
||||
// Move #info-modal to parent container
|
||||
if (!($('#history-modal').next().is('#info-modal'))) {
|
||||
|
@@ -692,7 +692,7 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Last Played</h4>
|
||||
<h4>Last Watched</h4>
|
||||
</div>
|
||||
<div class="home-platforms-instance-last-user">
|
||||
<h4>
|
||||
|
@@ -23,7 +23,7 @@
|
||||
<h3>Watch Statistics <small>Last ${config['home_stats_length']} days</small></h3>
|
||||
</div>
|
||||
<div id="home-stats" class="home-platforms">
|
||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,19 +36,27 @@
|
||||
<h3>Library Statistics <small>${config['pms_name']}</small></h3>
|
||||
</div>
|
||||
<div id="library-stats" class="library-platforms">
|
||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div class='row'>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="padded-header">
|
||||
<ul class="nav nav-header nav-dashboard pull-right">
|
||||
<li>
|
||||
<a href="#" id="recently-added-page-left" class="paginate btn-gray disabled" data-id="+1"><i class="fa fa-lg fa-chevron-left"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" id="recently-added-page-right" class="paginate btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Recently Added</h3>
|
||||
</div>
|
||||
<div id='recentlyAdded'>
|
||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
|
||||
<div id="recentlyAdded" style="margin-right: -15px;">
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +67,6 @@
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
<script>
|
||||
|
||||
function currentActivityHeader() {
|
||||
$.ajax({
|
||||
url: 'get_current_activity_header',
|
||||
@@ -71,7 +78,6 @@
|
||||
});
|
||||
}
|
||||
currentActivityHeader();
|
||||
setInterval(currentActivityHeader, 15000);
|
||||
|
||||
function currentActivity() {
|
||||
$.ajax({
|
||||
@@ -84,7 +90,12 @@
|
||||
});
|
||||
}
|
||||
currentActivity();
|
||||
setInterval(currentActivity, 15000);
|
||||
|
||||
setInterval(function () {
|
||||
$('.bar, .bufferbar').tooltip('destroy');
|
||||
currentActivityHeader();
|
||||
currentActivity();
|
||||
}, 15000);
|
||||
|
||||
function getHomeStats(days) {
|
||||
$.ajax({
|
||||
@@ -97,6 +108,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
getHomeStats();
|
||||
|
||||
function getLibraryStats() {
|
||||
$.ajax({
|
||||
@@ -109,33 +121,21 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
getLibraryStats();
|
||||
|
||||
function recentlyAdded() {
|
||||
var widthVal = $('body').find(".container-fluid").width();
|
||||
var tmp = (widthVal-20) / 182;
|
||||
|
||||
if (tmp > 0) {
|
||||
containerSize = parseInt(tmp);
|
||||
} else {
|
||||
containerSize = 1;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: 'get_recently_added',
|
||||
type: "GET",
|
||||
async: true,
|
||||
data: { count : containerSize },
|
||||
data: { count : 50 },
|
||||
complete: function(xhr, status) {
|
||||
$("#recentlyAdded").html(xhr.responseText);
|
||||
highlightAddedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
$(document).ready(function () {
|
||||
recentlyAdded();
|
||||
$(window).resize(function() {
|
||||
recentlyAdded();
|
||||
});
|
||||
});
|
||||
recentlyAdded();
|
||||
|
||||
var date_format = 'YYYY-MM-DD';
|
||||
var time_format = 'hh:mm a';
|
||||
@@ -148,10 +148,44 @@
|
||||
}
|
||||
});
|
||||
|
||||
getHomeStats();
|
||||
getLibraryStats();
|
||||
function highlightAddedScrollerButton() {
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("body").find(".container-fluid").width()) {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-added-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function () {
|
||||
highlightAddedScrollerButton();
|
||||
});
|
||||
|
||||
var leftTotal = 0;
|
||||
$(".paginate").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var containerWidth = $("body").find(".container-fluid").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt((containerWidth - 15) / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
$("#recently-added-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
$("#recently-added-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</%def>
|
||||
|
@@ -171,10 +171,10 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
% if data['media_type'] == 'movie' or data['media_type'] == 'episode' or data['media_type'] == 'track':
|
||||
<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" />
|
||||
% 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" />
|
||||
% endif
|
||||
% if data['audio_codec']:
|
||||
@@ -344,25 +344,41 @@ DOCUMENTATION :: END
|
||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||
<i class="fa fa-trash-o"></i> Delete mode
|
||||
</button>
|
||||
% if source == 'history':
|
||||
<a href="update_metadata?rating_key=${data['rating_key']}&update=True" class="btn btn-danger btn-edit" id="fix-metadata">
|
||||
<i class="fa fa-wrench"></i> Fix Metadata
|
||||
</a>
|
||||
% endif
|
||||
% if data.get('poster_url'):
|
||||
% if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||
<span class="imgur-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="80" data-width="80" style="display: inline-flex;">
|
||||
% else:
|
||||
<span class="imgur-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="120" data-width="80" style="display: inline-flex;">
|
||||
% endif
|
||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="delete-imgur-poster">
|
||||
<i class="fa fa-picture-o"></i> Reset Imgur Poster
|
||||
</button>
|
||||
</span>
|
||||
% endif
|
||||
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select rows to delete. Data is deleted upon exiting delete mode.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<table class="display history_table" id="history_table-RK-${data['rating_key']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align='left' id="delete">Delete</th>
|
||||
<th align='left' id="time">Time</th>
|
||||
<th align='left' id="friendly_name">User</th>
|
||||
<th align='left' id="ip_address">IP Address</th>
|
||||
<th align='left' id="platform">Platform</th>
|
||||
<th align='left' id="player">Player</th>
|
||||
<th align='left' id="title">Title</th>
|
||||
<th align='left' id="started">Started</th>
|
||||
<th align='left' id="paused_counter">Paused</th>
|
||||
<th align='left' id="stopped">Stopped</th>
|
||||
<th align='left' id="duration">Duration</th>
|
||||
<th align='left' id="percent_complete"></th>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
<th align="left" id="time">Time</th>
|
||||
<th align="left" id="friendly_name">User</th>
|
||||
<th align="left" id="ip_address">IP Address</th>
|
||||
<th align="left" id="platform">Platform</th>
|
||||
<th align="left" id="player">Player</th>
|
||||
<th align="left" id="title">Title</th>
|
||||
<th align="left" id="started">Started</th>
|
||||
<th align="left" id="paused_counter">Paused</th>
|
||||
<th align="left" id="stopped">Stopped</th>
|
||||
<th align="left" id="duration">Duration</th>
|
||||
<th align="left" id="percent_complete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -458,11 +474,11 @@ DOCUMENTATION :: END
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
get_history();
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
history_table = $('#history_table-RK-${data["rating_key"]}').DataTable(history_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
clearSearchButton('history_table-RK-${data["rating_key"]}', history_table);
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
@@ -519,10 +535,33 @@ DOCUMENTATION :: END
|
||||
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
</script>
|
||||
% if source == 'history':
|
||||
% if data.get('poster_url'):
|
||||
<script>
|
||||
$('#row-edit-mode').after('<a href="update_metadata?rating_key=${data['rating_key']}&update=True" class="btn btn-danger btn-edit" id="fix-metadata"> \
|
||||
<i class="fa fa-wrench"></i> Fix Metadata</a>');
|
||||
$('.imgur-poster-tooltip').popover({
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
placement: 'left',
|
||||
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||
content: function () {
|
||||
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
|
||||
}
|
||||
});
|
||||
|
||||
$('#delete-imgur-poster').on('click', function() {
|
||||
$.ajax({
|
||||
url: 'delete_poster_url',
|
||||
type: 'POST',
|
||||
async: true,
|
||||
data: { poster_url : "${data['poster_url']}" },
|
||||
success: function (data) {
|
||||
var msg = '<i class="fa fa-check"></i> Imgur poster reset';
|
||||
showMsg(msg, false, true, 2000);
|
||||
$('.imgur-poster-tooltip').popover('destroy');
|
||||
$('#delete-imgur-poster').closest('span').remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% endif
|
||||
|
@@ -32,7 +32,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="text-muted">Telize service written by <a href="https://github.com/fcambus/telize" target="_blank">Frederic Cambus</a>.</span>
|
||||
<% from plexpy.helpers import anon_url %>
|
||||
<span class="text-muted">Telize service written by <a href="${anon_url('https://github.com/fcambus/telize')}" target="_blank">Frederic Cambus</a>.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -28,8 +28,8 @@ var hc_plays_by_day_options = {
|
||||
cursor: 'pointer',
|
||||
point: {
|
||||
events: {
|
||||
click: function() {
|
||||
selectHandler(this.category);
|
||||
click: function () {
|
||||
selectHandler(this.category, this.series.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,8 @@ var hc_plays_by_day_options = {
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
shared: true
|
||||
shared: true,
|
||||
crosshairs: true
|
||||
},
|
||||
series: [{}]
|
||||
};
|
@@ -29,7 +29,7 @@ var hc_plays_by_stream_type_options = {
|
||||
point: {
|
||||
events: {
|
||||
click: function() {
|
||||
selectHandler(this.category);
|
||||
selectHandler(this.category, this.series.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,8 @@ var hc_plays_by_stream_type_options = {
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
shared: true
|
||||
shared: true,
|
||||
crosshairs: true
|
||||
},
|
||||
series: [{}]
|
||||
};
|
@@ -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
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
@@ -157,6 +157,9 @@ function doAjaxCall(url,elem,reload,form) {
|
||||
complete: function(jqXHR, textStatus) {
|
||||
// Remove loaders and stuff, ajax request is complete!
|
||||
loader.remove();
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -252,13 +255,13 @@ function isPrivateIP(ip_address) {
|
||||
|
||||
function humanTime(seconds) {
|
||||
if (seconds >= 86400) {
|
||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) +
|
||||
'</h3><p> days </p><h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) +
|
||||
'</h3><p> hrs</p><h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' +
|
||||
'<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' +
|
||||
'<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
return text;
|
||||
} else if (seconds >= 3600) {
|
||||
text = '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) +
|
||||
'</h3><p>hrs</p><h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
text = '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' +
|
||||
'<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
return text;
|
||||
} else if (seconds >= 60) {
|
||||
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 () {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
});
|
||||
};
|
@@ -107,7 +107,7 @@ history_table_options = {
|
||||
}
|
||||
},
|
||||
"width": "10%",
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [5],
|
||||
@@ -115,11 +115,11 @@ history_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode' || rowData['audio_decision'] === 'transcode') {
|
||||
if (rowData['transcode_decision'] === 'transcode') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'copy' || rowData['audio_decision'] === 'copy') {
|
||||
} else if (rowData['transcode_decision'] === 'copy') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'direct play' || rowData['audio_decision'] === 'direct play') {
|
||||
} else if (rowData['transcode_decision'] === 'direct play') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||
@@ -307,7 +307,7 @@ history_table_options = {
|
||||
}
|
||||
|
||||
// Parent table platform modal
|
||||
$('#history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = history_table.row( tr );
|
||||
var rowData = row.data();
|
||||
@@ -327,7 +327,7 @@ $('#history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||
});
|
||||
|
||||
// Parent table ip address modal
|
||||
$('#history_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
|
||||
$('.history_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = history_table.row( tr );
|
||||
var rowData = row.data();
|
||||
@@ -350,7 +350,7 @@ $('#history_table').on('click', '> tbody > tr > td.modal-control-ip', function (
|
||||
});
|
||||
|
||||
// Parent table delete mode
|
||||
$('#history_table').on('click', '> tbody > tr > td.delete-control > button', function () {
|
||||
$('.history_table').on('click', '> tbody > tr > td.delete-control > button', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = history_table.row( tr );
|
||||
var rowData = row.data();
|
||||
@@ -399,7 +399,7 @@ $('#history_table').on('click', '> tbody > tr > td.delete-control > button', fun
|
||||
});
|
||||
|
||||
// Parent table expand detailed history
|
||||
$('#history_table').on('click', '> tbody > tr > td.expand-history a', function () {
|
||||
$('.history_table').on('click', '> tbody > tr > td.expand-history a', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = history_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
@@ -79,11 +79,11 @@ history_table_modal_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode' || rowData['audio_decision'] === 'transcode') {
|
||||
if (rowData['transcode_decision'] === 'transcode') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'copy' || rowData['audio_decision'] === 'copy') {
|
||||
} else if (rowData['transcode_decision'] === 'copy') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'direct play' || rowData['audio_decision'] === 'direct play') {
|
||||
} else if (rowData['transcode_decision'] === 'direct play') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||
@@ -148,7 +148,7 @@ history_table_modal_options = {
|
||||
}
|
||||
}
|
||||
|
||||
$('#history_table').on('click', 'td.modal-control', function () {
|
||||
$('.history_table').on('click', 'td.modal-control', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = history_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
@@ -161,12 +161,28 @@ libraries_list_table_options = {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "25%",
|
||||
"width": "18%",
|
||||
"className": "hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [9],
|
||||
"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,
|
||||
"width": "10%"
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ media_info_table_options = {
|
||||
"emptyTable": "No data in table"
|
||||
},
|
||||
"pagingType": "bootstrap",
|
||||
"stateSave": false,
|
||||
"stateSave": true,
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
"pageLength": 25,
|
||||
@@ -34,9 +34,12 @@ media_info_table_options = {
|
||||
"targets": [0],
|
||||
"data": "added_at",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
if (rowData) {
|
||||
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') {
|
||||
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>');
|
||||
@@ -291,7 +294,7 @@ media_info_table_options = {
|
||||
}
|
||||
|
||||
// Parent table expand detailed media info
|
||||
$('#media_info_table').on('click', '> tbody > tr > td.expand-media-info a', function () {
|
||||
$('.media_info_table').on('click', '> tbody > tr > td.expand-media-info a', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = media_info_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
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": true,
|
||||
"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)
|
||||
}
|
||||
}
|
@@ -56,7 +56,7 @@ user_ip_table_options = {
|
||||
}
|
||||
},
|
||||
"width": "15%",
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
@@ -64,11 +64,11 @@ user_ip_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData) {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode' || rowData['audio_decision'] === 'transcode') {
|
||||
if (rowData['transcode_decision'] === 'transcode') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'copy' || rowData['audio_decision'] === 'copy') {
|
||||
} else if (rowData['transcode_decision'] === 'copy') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'direct play' || rowData['audio_decision'] === 'direct play') {
|
||||
} else if (rowData['transcode_decision'] === 'direct play') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||
@@ -146,11 +146,11 @@ user_ip_table_options = {
|
||||
}
|
||||
}
|
||||
|
||||
$('#user_ip_table').on('mouseenter', 'td.modal-control span', function () {
|
||||
$('.user_ip_table').on('mouseenter', 'td.modal-control span', function () {
|
||||
$(this).tooltip();
|
||||
});
|
||||
|
||||
$('#user_ip_table').on('click', 'td.modal-control', function () {
|
||||
$('.user_ip_table').on('click', 'td.modal-control', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = user_ip_table.row(tr);
|
||||
var rowData = row.data();
|
||||
@@ -169,7 +169,7 @@ $('#user_ip_table').on('click', 'td.modal-control', function () {
|
||||
showStreamDetails();
|
||||
});
|
||||
|
||||
$('#user_ip_table').on('click', 'td.modal-control-ip', function () {
|
||||
$('.user_ip_table').on('click', 'td.modal-control-ip', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = user_ip_table.row( tr );
|
||||
var rowData = row.data();
|
||||
|
@@ -120,11 +120,11 @@ users_list_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode' || rowData['audio_decision'] === 'transcode') {
|
||||
if (rowData['transcode_decision'] === 'transcode') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'copy' || rowData['audio_decision'] === 'copy') {
|
||||
} else if (rowData['transcode_decision'] === 'copy') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||
} else if (rowData['video_decision'] === 'direct play' || rowData['audio_decision'] === 'direct play') {
|
||||
} else if (rowData['transcode_decision'] === 'direct play') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||
@@ -165,12 +165,28 @@ users_list_table_options = {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "30%",
|
||||
"width": "23%",
|
||||
"className": "hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [8],
|
||||
"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,
|
||||
"width": "10%"
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
<%def name="headIncludes()">
|
||||
<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">
|
||||
</%def>
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
<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.
|
||||
<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>
|
||||
% endif
|
||||
<div class='table-card-header'>
|
||||
@@ -23,6 +24,7 @@
|
||||
<span><i class="fa fa-book"></i> All Libraries</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="colvis-button-bar hidden-xs"></div>
|
||||
% 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>
|
||||
% else:
|
||||
@@ -48,6 +50,7 @@
|
||||
<th align="left" id="last_accessed">Last Accessed</th>
|
||||
<th align="left" id="last_played">Last Played</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
<th align="left" id="total_duration">Total Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -79,6 +82,7 @@
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<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.pagination.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);
|
||||
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);
|
||||
|
||||
@@ -177,7 +183,7 @@
|
||||
$("#refresh-libraries-list").click(function () {
|
||||
if ("${config['update_section_ids']}" == "1") {
|
||||
$('#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 />' +
|
||||
'You may leave this page and come back later.');
|
||||
$(this).prop('disabled', true);
|
||||
|
@@ -37,7 +37,9 @@ DOCUMENTATION :: END
|
||||
% if data:
|
||||
<div class="container-fluid">
|
||||
<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>
|
||||
% endif
|
||||
<div class="summary-container">
|
||||
<div class="summary-navbar">
|
||||
<div class="col-md-12">
|
||||
@@ -52,7 +54,7 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-back">
|
||||
<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>
|
||||
% 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>
|
||||
@@ -116,8 +118,16 @@ DOCUMENTATION :: END
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<ul class="nav nav-header nav-dashboard pull-right">
|
||||
<li>
|
||||
<a href="#" id="recently-watched-page-left" class="paginate-watched btn-gray disabled" data-id="+1"><i class="fa fa-lg fa-chevron-left"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" id="recently-watched-page-right" class="paginate-watched btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-history"></i> Recently Watched</span>
|
||||
<span><i class="fa fa-history"></i> Recently Played</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
@@ -133,6 +143,14 @@ DOCUMENTATION :: END
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<ul class="nav nav-header nav-dashboard pull-right">
|
||||
<li>
|
||||
<a href="#" id="recently-added-page-left" class="paginate-added btn-gray disabled" data-id="+1"><i class="fa fa-lg fa-chevron-left"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" id="recently-added-page-right" class="paginate-added btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-history"></i> Recently Added</span>
|
||||
</div>
|
||||
@@ -168,7 +186,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<table class="display history_table" id="history_table-SID-${data['section_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
@@ -227,7 +245,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="media_info_table" width="100%">
|
||||
<table class="display media_info_table" id="media_info_table-SID-${data['section_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="added_at">Added At</th>
|
||||
@@ -362,12 +380,12 @@ DOCUMENTATION :: END
|
||||
};
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
|
||||
}
|
||||
|
||||
function loadMediaInfoTable() {
|
||||
@@ -383,12 +401,12 @@ DOCUMENTATION :: END
|
||||
};
|
||||
}
|
||||
}
|
||||
media_info_table = $('#media_info_table').DataTable(media_info_table_options);
|
||||
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-media-info');
|
||||
|
||||
clearSearchButton('media_info_table', media_info_table);
|
||||
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
|
||||
}
|
||||
|
||||
$( "#history-tab-btn" ).one( "click", function() {
|
||||
@@ -458,61 +476,116 @@ DOCUMENTATION :: END
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
var widthVal = $("#library-recently-watched").width();
|
||||
var tmp = (widthVal-25) / 175;
|
||||
|
||||
if (tmp > 0) {
|
||||
var containerSize = parseInt(tmp);
|
||||
} else {
|
||||
var containerSize = 1;
|
||||
}
|
||||
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_library_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
section_id: section_id,
|
||||
limit: containerSize
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#library-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recentlyAdded() {
|
||||
var widthVal = $("#library-recently-added").width();
|
||||
var tmp = (widthVal-25) / 175;
|
||||
|
||||
if (tmp > 0) {
|
||||
var containerSize = parseInt(tmp);
|
||||
} else {
|
||||
var containerSize = 1;
|
||||
}
|
||||
|
||||
// Populate recently added
|
||||
$.ajax({
|
||||
url: 'get_library_recently_added',
|
||||
async: true,
|
||||
data: {
|
||||
section_id: section_id,
|
||||
limit: containerSize
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#library-recently-added").html(xhr.responseText);
|
||||
highlightAddedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
recentlyAdded();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#library-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
function highlightAddedScrollerButton() {
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#library-recently-added").width()) {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-added-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
recentlyWatched();
|
||||
recentlyAdded();
|
||||
highlightWatchedScrollerButton();
|
||||
highlightAddedScrollerButton();
|
||||
});
|
||||
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
|
||||
var leftTotalWatched = 0;
|
||||
$(".paginate-watched").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#library-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotalWatched }, 250);
|
||||
|
||||
if (leftTotalWatched == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotalWatched == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
var leftTotalAdded = 0;
|
||||
$(".paginate-added").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var containerWidth = $("#library-recently-added").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotalAdded }, 250);
|
||||
|
||||
if (leftTotalAdded == 0) {
|
||||
$("#recently-added-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotalAdded == leftMax) {
|
||||
$("#recently-added-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
@@ -32,65 +32,67 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<li>
|
||||
% if item['media_type'] == 'episode' or item['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
% if item['media_type'] == 'episode':
|
||||
% if item['parent_thumb']:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
% else:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['grandparent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
% endif
|
||||
% elif item['media_type'] == 'movie':
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
% endif
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
<div id="recently-added-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<li>
|
||||
% if item['media_type'] == 'episode' or item['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
% if item['media_type'] == 'episode':
|
||||
% if item['parent_thumb']:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
% else:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['grandparent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
% endif
|
||||
% elif item['media_type'] == 'movie':
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
% endif
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'episode':
|
||||
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">S${item['parent_media_index']} · E${item['media_index']}</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
% elif item['media_type'] == 'album':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'episode':
|
||||
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">S${item['parent_media_index']} · E${item['media_index']}</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
% elif item['media_type'] == 'album':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||
<h3 class="text-muted">${item['title']}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||
<h3 class="text-muted">${item['title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="text-muted">Unable to retrieve data from database.
|
||||
<div class="text-muted">No stats to show.
|
||||
</div><br>
|
||||
% endif
|
@@ -75,13 +75,13 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% if library['thumb']:
|
||||
% if library['thumb'].startswith("http"):
|
||||
<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>
|
||||
% else:
|
||||
<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>
|
||||
% endif
|
||||
</li>
|
||||
|
@@ -22,6 +22,7 @@ from plexpy import helpers
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-dark" id="clear-logs"><i class="fa fa-trash-o"></i> Clear log</button>
|
||||
<button class="btn btn-dark" id="clear-notify-logs" style="display: none;"><i class="fa fa-trash-o"></i> Clear log</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
@@ -29,6 +30,8 @@ from plexpy import helpers
|
||||
<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"><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>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
||||
@@ -57,6 +60,33 @@ from plexpy import helpers
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
@@ -80,8 +110,10 @@ from plexpy import helpers
|
||||
<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.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/plex_logs.js"></script>
|
||||
<script src="interfaces/default/js/tables/notification_logs.js"></script>
|
||||
<script>
|
||||
|
||||
$(document).ready(function() {
|
||||
@@ -91,37 +123,86 @@ from plexpy import helpers
|
||||
|
||||
function LoadPlexPyLogs() {
|
||||
log_table_options.ajax = {
|
||||
"url": "getLog"
|
||||
url: "getLog"
|
||||
}
|
||||
log_table = $('#log_table').DataTable(log_table_options);
|
||||
}
|
||||
|
||||
function LoadPlexLogs() {
|
||||
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);
|
||||
}
|
||||
|
||||
$("#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-notify-logs").hide();
|
||||
LoadPlexPyLogs();
|
||||
clearSearchButton('log_table', log_table);
|
||||
});
|
||||
|
||||
$("#plex-logs-btn").click(function() {
|
||||
$("#plex-logs-btn").click(function () {
|
||||
$("#clear-logs").hide();
|
||||
$("#clear-notify-logs").hide();
|
||||
LoadPlexLogs();
|
||||
clearSearchButton('plex_log_table', plex_log_table);
|
||||
});
|
||||
|
||||
$("#clear-logs").click(function() {
|
||||
$("#plex-scanner-logs-btn").click(function () {
|
||||
$("#clear-logs").hide();
|
||||
$("#clear-notify-logs").hide();
|
||||
LoadPlexScannerLogs();
|
||||
clearSearchButton('plex_scanner_log_table', plex_scanner_log_table);
|
||||
});
|
||||
|
||||
$("#notification-logs-btn").click(function () {
|
||||
$("#clear-logs").hide();
|
||||
$("#clear-notify-logs").show();
|
||||
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?");
|
||||
if (r == true) {
|
||||
window.location.href = "clearLogs";
|
||||
}
|
||||
});
|
||||
|
||||
$("#clear-notify-logs").click(function () {
|
||||
var r = confirm("Are you sure you want to clear the PlexPy notification log?");
|
||||
if (r == true) {
|
||||
$.ajax({
|
||||
url: 'clearNotifyLogs',
|
||||
type: 'POST',
|
||||
success: function (data) {
|
||||
notification_log_table.draw();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var timer;
|
||||
function setRefresh()
|
||||
{
|
||||
@@ -137,8 +218,12 @@ from plexpy import helpers
|
||||
timer = setInterval(function() {
|
||||
if ($("#tabs-1").hasClass("active")) {
|
||||
log_table.ajax.reload();
|
||||
} else {
|
||||
} else if ($("#tabs-2").hasClass("active")) {
|
||||
plex_log_table.ajax.reload();
|
||||
} else if ($("#tabs-3").hasClass("active")) {
|
||||
plex_scanner_log_table.ajax.reload();
|
||||
} else if ($("#tabs-4").hasClass("active")) {
|
||||
notification_log_table.ajax.reload();
|
||||
}
|
||||
}, 1000*refreshrate.value);
|
||||
}
|
||||
|
@@ -132,7 +132,7 @@ from plexpy import helpers
|
||||
function reloadModal() {
|
||||
$.ajax({
|
||||
url: 'get_notification_agent_config',
|
||||
data: { config_id: '${agent["id"]}' },
|
||||
data: { agent_id: '${agent["id"]}' },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
@@ -147,9 +147,8 @@ from plexpy import helpers
|
||||
})
|
||||
|
||||
$('#save-notification-item').click(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;
|
||||
});
|
||||
|
||||
@@ -181,6 +180,10 @@ from plexpy import helpers
|
||||
});
|
||||
|
||||
$('#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);
|
||||
$.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); });
|
||||
@@ -191,7 +194,7 @@ from plexpy import helpers
|
||||
$.ajax({
|
||||
url: 'test_notifier',
|
||||
data: {
|
||||
config_id: '${agent["id"]}',
|
||||
agent_id: '${agent["id"]}',
|
||||
subject: $('#test_subject').val(),
|
||||
body: $('#test_body').val(),
|
||||
script: $('#test_script').val(),
|
||||
@@ -207,8 +210,8 @@ from plexpy import helpers
|
||||
});
|
||||
|
||||
$('#pushbullet_apikey, #pushover_apitoken, #scripts_folder').on('change', function () {
|
||||
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
||||
reloadModal();
|
||||
// Reload modal to update certain fields
|
||||
doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal);
|
||||
return false;
|
||||
});
|
||||
|
||||
|
@@ -64,13 +64,6 @@ from plexpy import helpers
|
||||
</label>
|
||||
<p class="help-block">Trigger notification when a media item is added to the Plex Media Server.</p>
|
||||
</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_intdown" ${helpers.checked(data['on_intdown'])} class="toggle-switches">
|
||||
@@ -78,6 +71,20 @@ from plexpy import helpers
|
||||
</label>
|
||||
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached internally.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<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">
|
||||
Notify on Plex server back up
|
||||
</label>
|
||||
<p class="help-block">Trigger notification when the Plex Media Server can be reached internally after being down.</p>
|
||||
</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">
|
||||
@@ -87,10 +94,10 @@ from plexpy import helpers
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<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">
|
||||
Notify on Plex server back up
|
||||
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_pmsupdate" ${helpers.checked(data['on_pmsupdate'])} class="toggle-switches">
|
||||
Notify on Plex update available
|
||||
</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 an update for the Plex Media Server is available.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -32,56 +32,58 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<div class="dashboard-recent-media-instance">
|
||||
<li>
|
||||
% if item['media_type'] == 'season' or item['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
<div id="recently-added-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<div class="dashboard-recent-media-instance">
|
||||
<li>
|
||||
% if item['media_type'] == 'season' or item['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'season':
|
||||
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||
<h3 class="text-muted">${item['title']}</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
% elif item['media_type'] == 'album':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'season':
|
||||
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||
<h3 class="text-muted">${item['title']}</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
% elif item['media_type'] == 'album':
|
||||
<a href="info?rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||
<h3 class="text-muted">${item['title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
</div>
|
||||
% endfor
|
||||
</ul>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||
<h3 class="text-muted">${item['title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
</div>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="text-muted">There was an error communicating with your Plex Server. Please check your <a href="settings">settings</a>.
|
||||
|
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>
|
File diff suppressed because it is too large
Load Diff
@@ -112,8 +112,16 @@ from plexpy import helpers
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<ul class="nav nav-header nav-dashboard pull-right">
|
||||
<li>
|
||||
<a href="#" id="recently-watched-page-left" class="paginate btn-gray disabled" data-id="+1"><i class="fa fa-lg fa-chevron-left"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" id="recently-watched-page-right" class="paginate btn-gray" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-history"></i> Recently Watched</span>
|
||||
<span><i class="fa fa-history"></i> Recently Played</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
@@ -140,7 +148,7 @@ from plexpy import helpers
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table id="user_ip_table" class="display" width="100%">
|
||||
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left">Last Seen</th>
|
||||
@@ -178,7 +186,7 @@ from plexpy import helpers
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<table class="display history_table" id="history_table-UID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
@@ -218,7 +226,7 @@ from plexpy import helpers
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="sync_table" width="100%">
|
||||
<table class="display" id="sync_table-UID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="state">State</th>
|
||||
@@ -348,13 +356,13 @@ from plexpy import helpers
|
||||
};
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
|
||||
history_table.column(2).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
clearSearchButton('history_table-UID-${data["user_id"]}', history_table);
|
||||
|
||||
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
|
||||
<label class="btn btn-dark active"> \
|
||||
@@ -397,9 +405,9 @@ from plexpy import helpers
|
||||
};
|
||||
}
|
||||
}
|
||||
user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
|
||||
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
|
||||
|
||||
clearSearchButton('user_ip_table', user_ip_table);
|
||||
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
|
||||
});
|
||||
|
||||
$( "#sync-tab-btn" ).one( "click", function() {
|
||||
@@ -410,13 +418,13 @@ from plexpy import helpers
|
||||
d.user_id = user_id;
|
||||
}
|
||||
}
|
||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
|
||||
sync_table.column(1).visible(false);
|
||||
|
||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
||||
|
||||
clearSearchButton('sync_table', sync_table);
|
||||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
||||
});
|
||||
|
||||
// Load edit user modal
|
||||
@@ -471,32 +479,60 @@ from plexpy import helpers
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
var widthVal = $("#user-recently-watched").width();
|
||||
var tmp = (widthVal-25) / 175;
|
||||
|
||||
if (tmp > 0) {
|
||||
var containerSize = parseInt(tmp);
|
||||
} else {
|
||||
var containerSize = 1;
|
||||
}
|
||||
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
user_id: user_id,
|
||||
limit: containerSize
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#user-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
recentlyWatched();
|
||||
highlightWatchedScrollerButton();
|
||||
});
|
||||
|
||||
var leftTotal = 0;
|
||||
$(".paginate").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#user-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@@ -28,56 +28,58 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<li>
|
||||
% if item['media_type'] == 'episode' or item['media_type'] == 'movie':
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||
<script>
|
||||
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
|
||||
</script>
|
||||
<div id="recently-watched-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<li>
|
||||
% if item['media_type'] == 'episode' or item['media_type'] == 'movie':
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||
<script>
|
||||
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'episode':
|
||||
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">S${item['parent_media_index']} · E${item['media_index']}</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
% elif item['media_type'] == 'track':
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||
<script>
|
||||
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
|
||||
</script>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'episode':
|
||||
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">S${item['parent_media_index']} · E${item['media_index']}</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
% elif item['media_type'] == 'track':
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||
<script>
|
||||
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['parent_title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||
<h3 title="${item['title']}">${item['title']}</h3>
|
||||
<h3 class="text-muted">${item['parent_title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="text-muted">No stats to show.</div><br>
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
<%def name="headIncludes()">
|
||||
<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">
|
||||
</%def>
|
||||
|
||||
@@ -12,6 +13,7 @@
|
||||
<span><i class="fa fa-group"></i> All Users</span>
|
||||
</div>
|
||||
<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-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-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_played">Last Played</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
<th align="left" id="total_duration">Total Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -67,6 +70,7 @@
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<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.pagination.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);
|
||||
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);
|
||||
|
||||
|
@@ -83,7 +83,7 @@ from plexpy import common
|
||||
<div class="col-xs-4">
|
||||
<div class="checkbox">
|
||||
<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>
|
||||
</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="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="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="check_github" id="check_github" value="1" checked>
|
||||
<input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
|
||||
@@ -243,7 +244,7 @@ from plexpy import common
|
||||
},
|
||||
render: {
|
||||
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) {
|
||||
// first item is rendered before initialization bug?
|
||||
@@ -253,7 +254,7 @@ from plexpy import common
|
||||
.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) {
|
||||
@@ -377,8 +378,8 @@ from plexpy import common
|
||||
var pms_ip = $("#pms_ip").val().trim();
|
||||
var pms_port = $("#pms_port").val().trim();
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").val();
|
||||
var pms_is_remote = $("#pms_is_remote").val();
|
||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
||||
$('#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').fadeIn('fast');
|
||||
},
|
||||
success: function (xml) {
|
||||
if ($(xml).find('MediaContainer').attr('machineIdentifier')) {
|
||||
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier'));
|
||||
success: function (json) {
|
||||
var machine_identifier = json;
|
||||
if (machine_identifier) {
|
||||
$("#pms_identifier").val(machine_identifier);
|
||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
pms_verified = true;
|
||||
|
@@ -33,12 +33,13 @@ load_rc_config ${name}
|
||||
: ${plexpy_dir:="/usr/local/plexpy"}
|
||||
: ${plexpy_chdir:="${plexpy_dir}"}
|
||||
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
|
||||
: ${plexpy_flags:=""}
|
||||
|
||||
status_cmd="${name}_status"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-f -p ${plexpy_pid} python ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
|
||||
command="${plexpy_dir}/PlexPy.py"
|
||||
command_args="--daemon --pidfile ${plexpy_pid} --quiet --nolaunch ${plexpy_flags}"
|
||||
|
||||
# Ensure user is root when running this script.
|
||||
if [ `id -u` != "0" ]; then
|
||||
|
5
init-scripts/init.freenas
Normal file → Executable file
5
init-scripts/init.freenas
Normal file → Executable file
@@ -33,12 +33,13 @@ load_rc_config ${name}
|
||||
: ${plexpy_dir:="/usr/local/share/plexpy"}
|
||||
: ${plexpy_chdir:="${plexpy_dir}"}
|
||||
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
|
||||
: ${plexpy_flags:=""}
|
||||
|
||||
status_cmd="${name}_status"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-f -p ${plexpy_pid} python2 ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
|
||||
command="${plexpy_dir}/PlexPy.py"
|
||||
command_args="--daemon --pidfile ${plexpy_pid} --quiet --nolaunch ${plexpy_flags}"
|
||||
|
||||
# Ensure user is root when running this script.
|
||||
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 -*-
|
||||
#
|
||||
# Copyright (C) Martin Sj<53>gren and AB Strakt 2001, All rights reserved
|
||||
# Copyright (C) Jean-Paul Calderone 2008, All rights reserved
|
||||
# This file is licenced under the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 or later (aka LGPL v2.1)
|
||||
# Please see LGPL2.1.txt for more information
|
||||
# Copyright (C) AB Strakt
|
||||
# Copyright (C) Jean-Paul Calderone
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Certificate generation module.
|
||||
"""
|
||||
|
||||
from OpenSSL import crypto
|
||||
import time
|
||||
|
||||
TYPE_RSA = crypto.TYPE_RSA
|
||||
TYPE_DSA = crypto.TYPE_DSA
|
||||
|
||||
serial = int(time.time())
|
||||
|
||||
|
||||
def createKeyPair(type, bits):
|
||||
"""
|
||||
Create a public/private key pair.
|
||||
|
||||
Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
|
||||
bits - Number of bits to use in the key
|
||||
Returns: The public/private key pair in a PKey object
|
||||
@@ -29,12 +24,11 @@ def createKeyPair(type, bits):
|
||||
pkey.generate_key(type, bits)
|
||||
return pkey
|
||||
|
||||
def createCertRequest(pkey, digest="md5", **name):
|
||||
def createCertRequest(pkey, digest="sha256", **name):
|
||||
"""
|
||||
Create a certificate 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
|
||||
arguments are:
|
||||
C - Country name
|
||||
@@ -49,18 +43,17 @@ def createCertRequest(pkey, digest="md5", **name):
|
||||
req = crypto.X509Req()
|
||||
subj = req.get_subject()
|
||||
|
||||
for (key,value) in name.items():
|
||||
for key, value in name.items():
|
||||
setattr(subj, key, value)
|
||||
|
||||
req.set_pubkey(pkey)
|
||||
req.sign(pkey, digest)
|
||||
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.
|
||||
|
||||
Arguments: req - Certificate reqeust to use
|
||||
Arguments: req - Certificate request to use
|
||||
issuerCert - The certificate of the issuer
|
||||
issuerKey - The private key of the issuer
|
||||
serial - Serial number for the certificate
|
||||
@@ -68,9 +61,11 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
|
||||
starts being valid
|
||||
notAfter - Timestamp (relative to now) when the certificate
|
||||
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
|
||||
"""
|
||||
issuerCert, issuerKey = issuerCertKey
|
||||
notBefore, notAfter = validityPeriod
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(serial)
|
||||
cert.gmtime_adj_notBefore(notBefore)
|
||||
@@ -80,3 +75,32 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
|
||||
cert.set_pubkey(req.get_pubkey())
|
||||
cert.sign(issuerKey, digest)
|
||||
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
|
||||
|
||||
CONFIG = None
|
||||
CONFIG_FILE = None
|
||||
|
||||
DB_FILE = None
|
||||
|
||||
@@ -73,17 +74,19 @@ UMASK = None
|
||||
|
||||
POLLING_FAILOVER = False
|
||||
|
||||
|
||||
def initialize(config_file):
|
||||
with INIT_LOCK:
|
||||
|
||||
global CONFIG
|
||||
global CONFIG_FILE
|
||||
global _INITIALIZED
|
||||
global CURRENT_VERSION
|
||||
global LATEST_VERSION
|
||||
global UMASK
|
||||
global POLLING_FAILOVER
|
||||
|
||||
CONFIG = plexpy.config.Config(config_file)
|
||||
CONFIG_FILE = config_file
|
||||
|
||||
assert CONFIG is not None
|
||||
|
||||
@@ -100,7 +103,7 @@ def initialize(config_file):
|
||||
if not CONFIG.HTTPS_KEY:
|
||||
CONFIG.HTTPS_KEY = os.path.join(DATA_DIR, 'server.key')
|
||||
|
||||
if not CONFIG.LOG_DIR.startswith(os.path.abspath(DATA_DIR)):
|
||||
if not CONFIG.LOG_DIR:
|
||||
CONFIG.LOG_DIR = os.path.join(DATA_DIR, 'logs')
|
||||
|
||||
if not os.path.exists(CONFIG.LOG_DIR):
|
||||
@@ -117,14 +120,21 @@ def initialize(config_file):
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
verbose=VERBOSE)
|
||||
|
||||
if not CONFIG.CACHE_DIR.startswith(os.path.abspath(DATA_DIR)):
|
||||
# Put the cache dir in the data dir for now
|
||||
if not CONFIG.BACKUP_DIR:
|
||||
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:
|
||||
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
|
||||
if not os.path.exists(CONFIG.CACHE_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.CACHE_DIR)
|
||||
except OSError as e:
|
||||
logger.error("Could not create cache dir '%s': %s", DATA_DIR, e)
|
||||
logger.error("Could not create cache dir '%s': %s", CACHE_DIR, e)
|
||||
|
||||
# Initialize the database
|
||||
logger.info('Checking to see if the database has all tables....')
|
||||
@@ -176,7 +186,7 @@ def initialize(config_file):
|
||||
plextv.refresh_users()
|
||||
|
||||
# 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()
|
||||
|
||||
# Store the original umask
|
||||
@@ -186,7 +196,6 @@ def initialize(config_file):
|
||||
_INITIALIZED = True
|
||||
return True
|
||||
|
||||
|
||||
def daemonize():
|
||||
if threading.activeCount() != 1:
|
||||
logger.warn(
|
||||
@@ -282,10 +291,10 @@ def initialize_scheduler():
|
||||
else:
|
||||
seconds = 0
|
||||
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.UPDATE_SECTION_IDS != -1:
|
||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs',
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
|
||||
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)
|
||||
|
||||
if CONFIG.NOTIFY_RECENTLY_ADDED:
|
||||
@@ -295,11 +304,18 @@ def initialize_scheduler():
|
||||
schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
if CONFIG.MONITOR_PMS_UPDATES:
|
||||
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
|
||||
hours=12, minutes=0, seconds=0)
|
||||
else:
|
||||
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# If we're not using websockets then fall back to polling
|
||||
@@ -322,6 +338,8 @@ def initialize_scheduler():
|
||||
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
|
||||
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
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
try:
|
||||
@@ -333,7 +351,7 @@ def initialize_scheduler():
|
||||
#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.
|
||||
Reschedule job if Interval Settings have changed.
|
||||
@@ -348,11 +366,11 @@ def schedule_job(function, name, hours=0, minutes=0, seconds=0):
|
||||
logger.info("Removed background task: %s", name)
|
||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||
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)
|
||||
elif hours > 0 or minutes > 0 or seconds > 0:
|
||||
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)
|
||||
|
||||
|
||||
@@ -377,7 +395,7 @@ def dbcheck():
|
||||
# sessions table :: This is a temp table that logs currently active sessions
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'session_key INTEGER, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, '
|
||||
'session_key INTEGER, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
|
||||
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
|
||||
'ip_address TEXT, machine_id TEXT, player TEXT, platform TEXT, title TEXT, parent_title TEXT, '
|
||||
'grandparent_title TEXT, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||
@@ -400,8 +418,8 @@ def dbcheck():
|
||||
|
||||
# session_history_media_info table :: This is a table which logs each session's media info
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS session_history_media_info (id INTEGER PRIMARY KEY, '
|
||||
'rating_key INTEGER, video_decision TEXT, audio_decision TEXT, duration INTEGER DEFAULT 0, width INTEGER, '
|
||||
'CREATE TABLE IF NOT EXISTS session_history_media_info (id INTEGER PRIMARY KEY, rating_key INTEGER, '
|
||||
'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, width INTEGER, '
|
||||
'height INTEGER, container TEXT, video_codec TEXT, audio_codec TEXT, bitrate INTEGER, video_resolution TEXT, '
|
||||
'video_framerate TEXT, aspect_ratio TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
|
||||
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
|
||||
@@ -430,10 +448,10 @@ def dbcheck():
|
||||
|
||||
# notify_log table :: This is a table which logs notifications sent
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
|
||||
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER, '
|
||||
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER, on_created INTEGER)'
|
||||
'CREATE TABLE IF NOT EXISTS notify_log (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)'
|
||||
)
|
||||
|
||||
# library_sections table :: This table keeps record of the servers library sections
|
||||
@@ -601,6 +619,15 @@ def dbcheck():
|
||||
'ALTER TABLE sessions ADD COLUMN section_id INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT stopped FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN stopped INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade session_history table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT reference_id FROM session_history')
|
||||
@@ -651,6 +678,21 @@ def dbcheck():
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN section_id INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT transcode_decision FROM session_history_media_info')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table session_history_media_info.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_media_info ADD COLUMN transcode_decision TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET transcode_decision = (CASE '
|
||||
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
||||
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
||||
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
||||
)
|
||||
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT do_notify FROM users')
|
||||
@@ -711,10 +753,57 @@ def dbcheck():
|
||||
'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)
|
||||
try:
|
||||
result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_library_sections_1")')
|
||||
if result and 'server_id' not in [row[2] for row in result]:
|
||||
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()
|
||||
if 'section_id INTEGER UNIQUE' in result[0]:
|
||||
logger.debug(u"Altering database. Removing unique constraint on section_id from library_sections table.")
|
||||
c_db.execute(
|
||||
'CREATE TABLE library_sections_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
@@ -739,7 +828,7 @@ def dbcheck():
|
||||
'ALTER TABLE library_sections_temp RENAME TO library_sections'
|
||||
)
|
||||
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:
|
||||
c_db.execute(
|
||||
'DROP TABLE library_sections_temp'
|
||||
@@ -747,10 +836,21 @@ def dbcheck():
|
||||
except:
|
||||
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)
|
||||
try:
|
||||
result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_users_2")')
|
||||
if result and 'username' in [row[2] for row in result]:
|
||||
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="users"').fetchone()
|
||||
if 'username TEXT NOT NULL UNIQUE' in result[0]:
|
||||
logger.debug(u"Altering database. Removing unique constraint on username from users table.")
|
||||
c_db.execute(
|
||||
'CREATE TABLE users_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
@@ -773,7 +873,7 @@ def dbcheck():
|
||||
'ALTER TABLE users_temp RENAME TO users'
|
||||
)
|
||||
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:
|
||||
c_db.execute(
|
||||
'DROP TABLE users_temp'
|
||||
@@ -790,6 +890,7 @@ def dbcheck():
|
||||
conn_db.commit()
|
||||
c_db.close()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False):
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
@@ -818,10 +919,17 @@ def shutdown(restart=False, update=False):
|
||||
if '--nolaunch' not in args:
|
||||
args += ['--nolaunch']
|
||||
logger.info('Restarting PlexPy with %s', args)
|
||||
os.execv(exe, args)
|
||||
|
||||
# os.execv fails with spaced names on Windows
|
||||
# https://bugs.python.org/issue19066
|
||||
if os.name == 'nt':
|
||||
subprocess.Popen(args, cwd=os.getcwd())
|
||||
else:
|
||||
os.execv(exe, args)
|
||||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
logger.debug(u"Generating UUID...")
|
||||
return uuid.uuid4().hex
|
||||
|
@@ -16,7 +16,7 @@
|
||||
import time
|
||||
import plexpy
|
||||
|
||||
from plexpy import logger, pmsconnect, activity_processor, threading, notification_handler, helpers
|
||||
from plexpy import logger, pmsconnect, activity_processor, threading, notification_handler, helpers, notifiers
|
||||
|
||||
|
||||
class ActivityHandler(object):
|
||||
@@ -57,9 +57,11 @@ class ActivityHandler(object):
|
||||
if self.is_valid_session() and self.get_live_session():
|
||||
logger.debug(u"PlexPy ActivityHandler :: Session %s has started." % str(self.get_session_key()))
|
||||
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=self.get_live_session(), notify_action='play')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_play'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=self.get_live_session(), notify_action='play')).start()
|
||||
|
||||
# Write the new session to our temp session table
|
||||
self.update_db_session()
|
||||
@@ -77,20 +79,24 @@ class ActivityHandler(object):
|
||||
if not force_stop:
|
||||
ap.set_session_state(session_key=self.get_session_key(),
|
||||
state=self.timeline['state'],
|
||||
view_offset=self.timeline['viewOffset'])
|
||||
view_offset=self.timeline['viewOffset'],
|
||||
stopped=int(time.time()))
|
||||
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='stop')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_stop'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='stop')).start()
|
||||
|
||||
# Write it to the history table
|
||||
monitor_proc = activity_processor.ActivityProcessor()
|
||||
monitor_proc.write_session_history(session=db_session)
|
||||
|
||||
# Remove the session from our temp session table
|
||||
logger.debug(u"PlexPy ActivityHandler :: Removing session %s from session queue" % str(self.get_session_key()))
|
||||
ap.delete_session(session_key=self.get_session_key())
|
||||
|
||||
def on_pause(self):
|
||||
@@ -109,9 +115,11 @@ class ActivityHandler(object):
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='pause')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_pause'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='pause')).start()
|
||||
|
||||
def on_resume(self):
|
||||
if self.is_valid_session():
|
||||
@@ -129,9 +137,11 @@ class ActivityHandler(object):
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='resume')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_resume'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='resume')).start()
|
||||
|
||||
def on_buffer(self):
|
||||
if self.is_valid_session():
|
||||
@@ -156,11 +166,14 @@ class ActivityHandler(object):
|
||||
(self.get_session_key(), 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 \
|
||||
time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT:
|
||||
if plexpy.CONFIG.BUFFER_THRESHOLD > 0 and (current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and \
|
||||
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())
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_stream, notify_action='buffer')).start()
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_buffer'] for d in notifiers.available_notification_agents()):
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_stream, notify_action='buffer')).start()
|
||||
|
||||
# This function receives events from our websocket connection
|
||||
def process(self):
|
||||
@@ -202,10 +215,17 @@ class ActivityHandler(object):
|
||||
|
||||
# Monitor if the stream has reached the watch percentage for notifications
|
||||
# The only purpose of this is for notifications
|
||||
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||
if progress_percent >= plexpy.CONFIG.NOTIFY_WATCHED_PERCENT and this_state != 'buffering':
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='watched')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
notify_agents = [d['id'] for d in notifiers.available_notification_agents() if d['on_watched']]
|
||||
# Get the current states for notifications from our db
|
||||
notified_agents = [d['agent_id'] for d in notification_handler.get_notify_state(session=db_session)
|
||||
if d['notify_action'] == 'watched'] if notify_agents else []
|
||||
|
||||
if any(a not in notified_agents for a in notify_agents):
|
||||
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||
if progress_percent >= plexpy.CONFIG.NOTIFY_WATCHED_PERCENT and this_state != 'buffering':
|
||||
# Rather not put this on it's own thread so we know it completes before our next event.
|
||||
notification_handler.notify(stream_data=db_session, notify_action='watched')
|
||||
|
||||
else:
|
||||
# We don't have this session in our table yet, start a new one.
|
||||
|
@@ -13,7 +13,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger, pmsconnect, plextv, notification_handler, database, helpers, activity_processor, libraries
|
||||
from plexpy import logger, pmsconnect, plextv, notification_handler, database, helpers, activity_processor, libraries, notifiers
|
||||
|
||||
import threading
|
||||
import plexpy
|
||||
@@ -38,9 +38,12 @@ def check_active_sessions(ws_request=False):
|
||||
if session_list:
|
||||
if int_ping_count >= 3:
|
||||
logger.info(u"PlexPy Monitor :: The Plex Media Server is back up.")
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='intup')).start()
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_intup'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='intup')).start()
|
||||
int_ping_count = 0
|
||||
|
||||
media_container = session_list['sessions']
|
||||
@@ -58,16 +61,24 @@ def check_active_sessions(ws_request=False):
|
||||
# Here we can check the play states
|
||||
if session['state'] != stream['state']:
|
||||
if session['state'] == 'paused':
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='pause')).start()
|
||||
logger.debug(u"PlexPy Monitor :: Session %s has been paused." % stream['session_key'])
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_pause'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='pause')).start()
|
||||
|
||||
if session['state'] == 'playing' and stream['state'] == 'paused':
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='resume')).start()
|
||||
logger.debug(u"PlexPy Monitor :: Session %s has been resumed." % stream['session_key'])
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_resume'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='resume')).start()
|
||||
|
||||
if stream['state'] == 'paused' and not ws_request:
|
||||
# The stream is still paused so we need to increment the paused_counter
|
||||
@@ -105,8 +116,12 @@ def check_active_sessions(ws_request=False):
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_buffer'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||
else:
|
||||
# Subsequent buffer notifications after wait time
|
||||
if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \
|
||||
@@ -119,11 +134,16 @@ def check_active_sessions(ws_request=False):
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_buffer'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||
|
||||
logger.debug(u"PlexPy Monitor :: Stream buffering. Count is now %s. Last triggered %s."
|
||||
% (buffer_values[0]['buffer_count'],
|
||||
logger.debug(u"PlexPy Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||
% (stream['session_key'],
|
||||
buffer_values[0]['buffer_count'],
|
||||
buffer_values[0]['buffer_last_triggered']))
|
||||
|
||||
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
||||
@@ -132,37 +152,61 @@ def check_active_sessions(ws_request=False):
|
||||
if session['view_offset'] and session['duration'] and session['state'] != 'buffering':
|
||||
if helpers.get_percent(session['view_offset'],
|
||||
session['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_watched'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
||||
|
||||
else:
|
||||
# The user has stopped playing a stream
|
||||
if stream['state'] != 'stopped':
|
||||
logger.debug(u"PlexPy Monitor :: Session %s has stopped." % stream['session_key'])
|
||||
|
||||
# Set the stream stop time
|
||||
stream['stopped'] = int(time.time())
|
||||
monitor_db.action('UPDATE sessions SET stopped = ?, state = ? '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['stopped'], 'stopped', stream['session_key'], stream['rating_key']])
|
||||
|
||||
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
||||
if stream['view_offset'] and stream['duration']:
|
||||
if helpers.get_percent(stream['view_offset'],
|
||||
stream['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_watched'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
||||
|
||||
else:
|
||||
# The user has stopped playing a stream
|
||||
logger.debug(u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (stream['session_key'], stream['rating_key']))
|
||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
||||
if stream['view_offset'] and stream['duration']:
|
||||
if helpers.get_percent(stream['view_offset'],
|
||||
stream['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_stop'] for d in notifiers.available_notification_agents()):
|
||||
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
||||
|
||||
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='stop')).start()
|
||||
kwargs=dict(stream_data=stream, notify_action='stop')).start()
|
||||
|
||||
# Write the item history on playback stop
|
||||
monitor_process.write_session_history(session=stream)
|
||||
success = monitor_process.write_session_history(session=stream)
|
||||
|
||||
if success:
|
||||
# If session is written to the databaase successfully, remove the session from the session table
|
||||
logger.debug(u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (stream['session_key'], stream['rating_key']))
|
||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
else:
|
||||
logger.warn(u"PlexPy Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again on the next pass." % (stream['session_key'], stream['rating_key']))
|
||||
|
||||
# Process the newly received session data
|
||||
for session in media_container:
|
||||
monitor_process.write_session(session)
|
||||
new_session = monitor_process.write_session(session)
|
||||
|
||||
if new_session:
|
||||
logger.debug(u"PlexPy Monitor :: Session %s has started." % session['session_key'])
|
||||
|
||||
else:
|
||||
logger.debug(u"PlexPy Monitor :: Unable to read session list.")
|
||||
|
||||
@@ -171,9 +215,11 @@ def check_active_sessions(ws_request=False):
|
||||
% str(int_ping_count))
|
||||
|
||||
if int_ping_count == 3:
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='intdown')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_intdown'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='intdown')).start()
|
||||
|
||||
|
||||
def check_recently_added():
|
||||
@@ -225,9 +271,12 @@ def check_recently_added():
|
||||
|
||||
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(timeline_data=item, notify_action='created')).start()
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_created'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(timeline_data=item, notify_action='created')).start()
|
||||
|
||||
else:
|
||||
item = max(metadata, key=lambda x:x['added_at'])
|
||||
@@ -243,9 +292,12 @@ def check_recently_added():
|
||||
% str(item['rating_key']))
|
||||
|
||||
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(timeline_data=item, notify_action='created')).start()
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_created'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(timeline_data=item, notify_action='created')).start()
|
||||
|
||||
def check_server_response():
|
||||
|
||||
@@ -275,12 +327,44 @@ def check_server_response():
|
||||
else:
|
||||
if ext_ping_count >= 3:
|
||||
logger.info(u"PlexPy Monitor :: Plex remote access is back up.")
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='extup')).start()
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_extup'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='extup')).start()
|
||||
ext_ping_count = 0
|
||||
|
||||
if ext_ping_count == 3:
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='extdown')).start()
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_extdown'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='extdown')).start()
|
||||
|
||||
|
||||
def check_server_updates():
|
||||
|
||||
with monitor_lock:
|
||||
logger.info(u"PlexPy Monitor :: Checking for PMS updates...")
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
|
||||
server_identity = pms_connect.get_server_identity()
|
||||
update_status = pms_connect.get_update_staus()
|
||||
|
||||
if server_identity and update_status:
|
||||
version = server_identity['version']
|
||||
logger.info(u"PlexPy Monitor :: Current PMS version: %s", version)
|
||||
|
||||
if update_status['state'] == 'available':
|
||||
update_version = update_status['version']
|
||||
logger.info(u"PlexPy Monitor :: PMS update available version: %s", update_version)
|
||||
|
||||
# Check if any notification agents have notifications enabled
|
||||
if any(d['on_pmsupdate'] for d in notifiers.available_notification_agents()):
|
||||
# Fire off notifications
|
||||
threading.Thread(target=notification_handler.notify_timeline,
|
||||
kwargs=dict(notify_action='pmsupdate')).start()
|
||||
else:
|
||||
logger.info(u"PlexPy Monitor :: No PMS update available.")
|
@@ -13,7 +13,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger, pmsconnect, notification_handler, log_reader, database
|
||||
from plexpy import logger, pmsconnect, notification_handler, log_reader, database, notifiers
|
||||
|
||||
import threading
|
||||
import plexpy
|
||||
@@ -78,9 +78,10 @@ class ActivityProcessor(object):
|
||||
result = self.db.upsert('sessions', values, keys)
|
||||
|
||||
if result == 'insert':
|
||||
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
||||
if notify:
|
||||
# Check if any notification agents have notifications enabled
|
||||
if notify and any(d['on_play'] for d in notifiers.available_notification_agents()):
|
||||
values.update({'ip_address': session['ip_address']})
|
||||
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=values, notify_action='play')).start()
|
||||
|
||||
@@ -97,16 +98,23 @@ class ActivityProcessor(object):
|
||||
ip_address = {'ip_address': ip_address}
|
||||
self.db.upsert('sessions', ip_address, keys)
|
||||
|
||||
return True
|
||||
|
||||
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
|
||||
from plexpy import users, libraries
|
||||
|
||||
user_data = users.Users()
|
||||
user_details = user_data.get_details(user_id=session['user_id'])
|
||||
|
||||
section_id = session['section_id'] if not is_import else import_metadata['section_id']
|
||||
|
||||
library_data = libraries.Libraries()
|
||||
library_details = library_data.get_details(section_id=section_id)
|
||||
if not is_import:
|
||||
user_data = users.Users()
|
||||
user_details = user_data.get_details(user_id=session['user_id'])
|
||||
|
||||
library_data = libraries.Libraries()
|
||||
library_details = library_data.get_details(section_id=section_id)
|
||||
|
||||
# Return false if failed to retrieve user or library details
|
||||
if not user_details or not library_details:
|
||||
return False
|
||||
|
||||
if session:
|
||||
logging_enabled = False
|
||||
@@ -116,8 +124,14 @@ class ActivityProcessor(object):
|
||||
stopped = int(session['stopped'])
|
||||
else:
|
||||
stopped = int(time.time())
|
||||
elif session['stopped']:
|
||||
stopped = int(session['stopped'])
|
||||
else:
|
||||
stopped = int(time.time())
|
||||
self.set_session_state(session_key=session['session_key'],
|
||||
state='stopped',
|
||||
view_offset=session['viewOffset'],
|
||||
stopped=stopped)
|
||||
|
||||
if plexpy.CONFIG.MOVIE_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
|
||||
session['media_type'] == 'movie':
|
||||
@@ -137,14 +151,14 @@ class ActivityProcessor(object):
|
||||
else:
|
||||
real_play_time = stopped - session['started']
|
||||
|
||||
if plexpy.CONFIG.LOGGING_IGNORE_INTERVAL and not is_import:
|
||||
if not is_import and plexpy.CONFIG.LOGGING_IGNORE_INTERVAL:
|
||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||
logging_enabled = False
|
||||
logger.debug(u"PlexPy ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||
u"seconds, so we're not logging it." %
|
||||
(session['rating_key'], str(real_play_time), plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||
if session['media_type'] == 'track' and not is_import:
|
||||
if not is_import and session['media_type'] == 'track':
|
||||
if real_play_time < 15 and session['duration'] >= 30:
|
||||
logging_enabled = False
|
||||
logger.debug(u"PlexPy ActivityProcessor :: Play duration for ratingKey %s is %s secs, "
|
||||
@@ -156,17 +170,29 @@ class ActivityProcessor(object):
|
||||
logging_enabled = False
|
||||
logger.debug(u"PlexPy ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||
u"seconds, so we're not logging it." %
|
||||
(session['rating_key'], str(real_play_time),
|
||||
import_ignore_interval))
|
||||
(session['rating_key'], str(real_play_time), import_ignore_interval))
|
||||
|
||||
if not user_details['keep_history'] and not is_import:
|
||||
if not is_import and not user_details['keep_history']:
|
||||
logging_enabled = False
|
||||
logger.debug(u"PlexPy ActivityProcessor :: History logging for user '%s' is disabled." % user_details['username'])
|
||||
elif not library_details['keep_history'] and not is_import:
|
||||
elif not is_import and not library_details['keep_history']:
|
||||
logging_enabled = False
|
||||
logger.debug(u"PlexPy ActivityProcessor :: History logging for library '%s' is disabled." % library_details['section_name'])
|
||||
|
||||
if logging_enabled:
|
||||
|
||||
# Fetch metadata first so we can return false if it fails
|
||||
if not is_import:
|
||||
logger.debug(u"PlexPy ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||
if result:
|
||||
metadata = result['metadata']
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
metadata = import_metadata
|
||||
|
||||
# logger.debug(u"PlexPy ActivityProcessor :: Attempting to write to session_history table...")
|
||||
query = 'INSERT INTO session_history (started, stopped, rating_key, parent_rating_key, ' \
|
||||
'grandparent_rating_key, media_type, user_id, user, ip_address, paused_counter, player, ' \
|
||||
@@ -182,7 +208,7 @@ class ActivityProcessor(object):
|
||||
self.db.action(query=query, args=args)
|
||||
|
||||
# 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 '
|
||||
|
||||
args = [session['user_id']]
|
||||
@@ -191,6 +217,7 @@ class ActivityProcessor(object):
|
||||
|
||||
new_session = {'id': result[0]['id'],
|
||||
'rating_key': result[0]['rating_key'],
|
||||
'view_offset': result[0]['view_offset'],
|
||||
'user_id': result[0]['user_id'],
|
||||
'reference_id': result[0]['reference_id']}
|
||||
|
||||
@@ -199,12 +226,14 @@ class ActivityProcessor(object):
|
||||
else:
|
||||
prev_session = {'id': result[1]['id'],
|
||||
'rating_key': result[1]['rating_key'],
|
||||
'view_offset': result[1]['view_offset'],
|
||||
'user_id': result[1]['user_id'],
|
||||
'reference_id': result[1]['reference_id']}
|
||||
|
||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
||||
if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key']):
|
||||
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']]
|
||||
else:
|
||||
args = [new_session['id'], new_session['id']]
|
||||
@@ -215,13 +244,22 @@ class ActivityProcessor(object):
|
||||
# % last_id)
|
||||
|
||||
# Write the session_history_media_info table
|
||||
|
||||
# Generate a combined transcode decision value
|
||||
if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode':
|
||||
transcode_decision = 'transcode'
|
||||
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
|
||||
transcode_decision = 'copy'
|
||||
else:
|
||||
transcode_decision = 'direct play'
|
||||
|
||||
# logger.debug(u"PlexPy ActivityProcessor :: Attempting to write to session_history_media_info table...")
|
||||
query = 'INSERT INTO session_history_media_info (id, rating_key, video_decision, audio_decision, ' \
|
||||
'duration, width, height, container, video_codec, audio_codec, bitrate, video_resolution, ' \
|
||||
'video_framerate, aspect_ratio, audio_channels, transcode_protocol, transcode_container, ' \
|
||||
'transcode_video_codec, transcode_audio_codec, transcode_audio_channels, transcode_width, ' \
|
||||
'transcode_height) VALUES ' \
|
||||
'(last_insert_rowid(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
'transcode_height, transcode_decision) VALUES ' \
|
||||
'(last_insert_rowid(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
|
||||
args = [session['rating_key'], session['video_decision'], session['audio_decision'],
|
||||
session['duration'], session['width'], session['height'], session['container'],
|
||||
@@ -229,19 +267,12 @@ class ActivityProcessor(object):
|
||||
session['video_resolution'], session['video_framerate'], session['aspect_ratio'],
|
||||
session['audio_channels'], session['transcode_protocol'], session['transcode_container'],
|
||||
session['transcode_video_codec'], session['transcode_audio_codec'],
|
||||
session['transcode_audio_channels'], session['transcode_width'], session['transcode_height']]
|
||||
session['transcode_audio_channels'], session['transcode_width'], session['transcode_height'],
|
||||
transcode_decision]
|
||||
|
||||
# logger.debug(u"PlexPy ActivityProcessor :: Writing session_history_media_info transaction...")
|
||||
self.db.action(query=query, args=args)
|
||||
|
||||
if not is_import:
|
||||
logger.debug(u"PlexPy ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||
metadata = result['metadata']
|
||||
else:
|
||||
metadata = import_metadata
|
||||
|
||||
# Write the session_history_metadata table
|
||||
directors = ";".join(metadata['directors'])
|
||||
writers = ";".join(metadata['writers'])
|
||||
@@ -277,6 +308,9 @@ class ActivityProcessor(object):
|
||||
# logger.debug(u"PlexPy ActivityProcessor :: Writing session_history_metadata transaction...")
|
||||
self.db.action(query=query, args=args)
|
||||
|
||||
# Return true when the session is successfully written to the database
|
||||
return True
|
||||
|
||||
def find_session_ip(self, rating_key=None, machine_id=None):
|
||||
|
||||
logger.debug(u"PlexPy ActivityProcessor :: Requesting log lines...")
|
||||
@@ -348,12 +382,15 @@ class ActivityProcessor(object):
|
||||
|
||||
return None
|
||||
|
||||
def set_session_state(self, session_key=None, state=None, view_offset=0):
|
||||
def set_session_state(self, session_key=None, state=None, view_offset=0, **kwargs):
|
||||
if str(session_key).isdigit() and str(view_offset).isdigit():
|
||||
values = {'view_offset': int(view_offset)}
|
||||
if state:
|
||||
values['state'] = state
|
||||
|
||||
for k,v in kwargs.iteritems():
|
||||
values[k] = v
|
||||
|
||||
keys = {'session_key': session_key}
|
||||
result = self.db.upsert('sessions', values, keys)
|
||||
|
||||
|
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,16 @@ MEDIA_FLAGS_AUDIO = {'ac.?3': 'dolby_digital',
|
||||
MEDIA_FLAGS_VIDEO = {'avc1': 'h264',
|
||||
'wmv(1|2)': 'wmv',
|
||||
'wmv3': 'wmvhd'
|
||||
}
|
||||
}
|
||||
|
||||
SCHEDULER_LIST = ['Check GitHub for updates',
|
||||
'Check for active sessions',
|
||||
'Check for recently added items',
|
||||
'Check for Plex updates',
|
||||
'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_PORT': (int, 'PMS', 32400),
|
||||
'PMS_TOKEN': (str, 'PMS', ''),
|
||||
'PMS_SSL': (int, 'General', 0),
|
||||
'PMS_SSL': (int, 'PMS', 0),
|
||||
'PMS_URL': (str, 'PMS', ''),
|
||||
'PMS_USE_BIF': (int, 'PMS', 0),
|
||||
'PMS_UUID': (str, 'PMS', ''),
|
||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||
'ANON_REDIRECT': (str, 'General', 'http://dereferer.org/?'),
|
||||
'API_ENABLED': (int, 'General', 0),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
'API_SQL': (int, 'General', 0),
|
||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
||||
'BOXCAR_SOUND': (str, 'Boxcar', ''),
|
||||
@@ -46,8 +48,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'BOXCAR_ON_INTDOWN': (int, 'Boxcar', 0),
|
||||
'BOXCAR_ON_EXTUP': (int, 'Boxcar', 0),
|
||||
'BOXCAR_ON_INTUP': (int, 'Boxcar', 0),
|
||||
'BOXCAR_ON_PMSUPDATE': (int, 'Boxcar', 0),
|
||||
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
|
||||
'BUFFER_WAIT': (int, 'Monitoring', 900),
|
||||
'BACKUP_DIR': (str, 'General', ''),
|
||||
'CACHE_DIR': (str, 'General', ''),
|
||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||
'CHECK_GITHUB': (int, 'General', 1),
|
||||
@@ -78,6 +82,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'EMAIL_ON_INTDOWN': (int, 'Email', 0),
|
||||
'EMAIL_ON_EXTUP': (int, 'Email', 0),
|
||||
'EMAIL_ON_INTUP': (int, 'Email', 0),
|
||||
'EMAIL_ON_PMSUPDATE': (int, 'Email', 0),
|
||||
'ENABLE_HTTPS': (int, 'General', 0),
|
||||
'FACEBOOK_ENABLED': (int, 'Facebook', 0),
|
||||
'FACEBOOK_REDIRECT_URI': (str, 'Facebook', ''),
|
||||
@@ -85,6 +90,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'FACEBOOK_APP_SECRET': (str, 'Facebook', ''),
|
||||
'FACEBOOK_TOKEN': (str, 'Facebook', ''),
|
||||
'FACEBOOK_GROUP': (str, 'Facebook', ''),
|
||||
'FACEBOOK_INCL_PMSLINK': (int, 'Facebook', 0),
|
||||
'FACEBOOK_INCL_POSTER': (int, 'Facebook', 1),
|
||||
'FACEBOOK_INCL_SUBJECT': (int, 'Facebook', 1),
|
||||
'FACEBOOK_ON_PLAY': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_STOP': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_PAUSE': (int, 'Facebook', 0),
|
||||
@@ -96,6 +104,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'FACEBOOK_ON_INTDOWN': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_EXTUP': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_INTUP': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_PMSUPDATE': (int, 'Facebook', 0),
|
||||
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
||||
'FREEZE_DB': (int, 'General', 0),
|
||||
'GET_FILE_SIZES': (int, 'General', 0),
|
||||
@@ -121,14 +130,18 @@ _CONFIG_DEFINITIONS = {
|
||||
'GROWL_ON_INTDOWN': (int, 'Growl', 0),
|
||||
'GROWL_ON_EXTUP': (int, 'Growl', 0),
|
||||
'GROWL_ON_INTUP': (int, 'Growl', 0),
|
||||
'GROWL_ON_PMSUPDATE': (int, 'Growl', 0),
|
||||
'HOME_LIBRARY_CARDS': (list, 'General', ['first_run']),
|
||||
'HOME_STATS_LENGTH': (int, 'General', 30),
|
||||
'HOME_STATS_TYPE': (int, 'General', 0),
|
||||
'HOME_STATS_COUNT': (int, 'General', 5),
|
||||
'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']),
|
||||
'HTTPS_CREATE_CERT': (int, 'General', 1),
|
||||
'HTTPS_CERT': (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_PASSWORD': (str, 'General', ''),
|
||||
'HTTP_PORT': (int, 'General', 8181),
|
||||
@@ -151,6 +164,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'IFTTT_ON_INTDOWN': (int, 'IFTTT', 0),
|
||||
'IFTTT_ON_EXTUP': (int, 'IFTTT', 0),
|
||||
'IFTTT_ON_INTUP': (int, 'IFTTT', 0),
|
||||
'IFTTT_ON_PMSUPDATE': (int, 'IFTTT', 0),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||
'LOG_DIR': (str, 'General', ''),
|
||||
@@ -165,6 +179,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||
'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
||||
'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
||||
'MONITOR_PMS_UPDATES': (int, 'Monitoring', 0),
|
||||
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
|
||||
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
|
||||
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
|
||||
@@ -182,7 +197,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'NMA_ON_INTDOWN': (int, 'NMA', 0),
|
||||
'NMA_ON_EXTUP': (int, 'NMA', 0),
|
||||
'NMA_ON_INTUP': (int, 'NMA', 0),
|
||||
'NMA_ON_PMSUPDATE': (int, 'NMA', 0),
|
||||
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
|
||||
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
||||
@@ -209,6 +226,8 @@ _CONFIG_DEFINITIONS = {
|
||||
'NOTIFY_ON_EXTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is back up.'),
|
||||
'NOTIFY_ON_INTUP_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||
'NOTIFY_ON_INTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is back up.'),
|
||||
'NOTIFY_ON_PMSUPDATE_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||
'NOTIFY_ON_PMSUPDATE_BODY_TEXT': (unicode, 'Monitoring', 'An update is available for the Plex Media Server (version {update_version}).'),
|
||||
'NOTIFY_SCRIPTS_ARGS_TEXT': (unicode, 'Monitoring', ''),
|
||||
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
|
||||
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
||||
@@ -223,6 +242,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'OSX_NOTIFY_ON_INTDOWN': (int, 'OSX_Notify', 0),
|
||||
'OSX_NOTIFY_ON_EXTUP': (int, 'OSX_Notify', 0),
|
||||
'OSX_NOTIFY_ON_INTUP': (int, 'OSX_Notify', 0),
|
||||
'OSX_NOTIFY_ON_PMSUPDATE': (int, 'OSX_Notify', 0),
|
||||
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
|
||||
'PLEX_ENABLED': (int, 'Plex', 0),
|
||||
'PLEX_PASSWORD': (str, 'Plex', ''),
|
||||
@@ -238,6 +258,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PLEX_ON_INTDOWN': (int, 'Plex', 0),
|
||||
'PLEX_ON_EXTUP': (int, 'Plex', 0),
|
||||
'PLEX_ON_INTUP': (int, 'Plex', 0),
|
||||
'PLEX_ON_PMSUPDATE': (int, 'Plex', 0),
|
||||
'PROWL_ENABLED': (int, 'Prowl', 0),
|
||||
'PROWL_KEYS': (str, 'Prowl', ''),
|
||||
'PROWL_PRIORITY': (int, 'Prowl', 0),
|
||||
@@ -252,6 +273,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PROWL_ON_INTDOWN': (int, 'Prowl', 0),
|
||||
'PROWL_ON_EXTUP': (int, 'Prowl', 0),
|
||||
'PROWL_ON_INTUP': (int, 'Prowl', 0),
|
||||
'PROWL_ON_PMSUPDATE': (int, 'Prowl', 0),
|
||||
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
|
||||
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
|
||||
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
|
||||
@@ -265,6 +287,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHALOT_ON_INTDOWN': (int, 'Pushalot', 0),
|
||||
'PUSHALOT_ON_EXTUP': (int, 'Pushalot', 0),
|
||||
'PUSHALOT_ON_INTUP': (int, 'Pushalot', 0),
|
||||
'PUSHALOT_ON_PMSUPDATE': (int, 'Pushalot', 0),
|
||||
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
|
||||
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
|
||||
'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''),
|
||||
@@ -280,8 +303,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHBULLET_ON_INTDOWN': (int, 'PushBullet', 0),
|
||||
'PUSHBULLET_ON_EXTUP': (int, 'PushBullet', 0),
|
||||
'PUSHBULLET_ON_INTUP': (int, 'PushBullet', 0),
|
||||
'PUSHBULLET_ON_PMSUPDATE': (int, 'PushBullet', 0),
|
||||
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
|
||||
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
|
||||
'PUSHOVER_HTML_SUPPORT': (int, 'Pushover', 1),
|
||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'PUSHOVER_SOUND': (str, 'Pushover', ''),
|
||||
@@ -296,6 +321,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHOVER_ON_INTDOWN': (int, 'Pushover', 0),
|
||||
'PUSHOVER_ON_EXTUP': (int, 'Pushover', 0),
|
||||
'PUSHOVER_ON_INTUP': (int, 'Pushover', 0),
|
||||
'PUSHOVER_ON_PMSUPDATE': (int, 'Pushover', 0),
|
||||
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
|
||||
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
|
||||
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
|
||||
@@ -304,6 +330,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'SLACK_HOOK': (str, 'Slack', ''),
|
||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||
'SLACK_ICON_EMOJI': (str, 'Slack', ''),
|
||||
'SLACK_INCL_SUBJECT': (int, 'Slack', 1),
|
||||
'SLACK_USERNAME': (str, 'Slack', ''),
|
||||
'SLACK_ON_PLAY': (int, 'Slack', 0),
|
||||
'SLACK_ON_STOP': (int, 'Slack', 0),
|
||||
@@ -316,6 +343,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'SLACK_ON_INTDOWN': (int, 'Slack', 0),
|
||||
'SLACK_ON_EXTUP': (int, 'Slack', 0),
|
||||
'SLACK_ON_INTUP': (int, 'Slack', 0),
|
||||
'SLACK_ON_PMSUPDATE': (int, 'Slack', 0),
|
||||
'SCRIPTS_ENABLED': (int, 'Scripts', 0),
|
||||
'SCRIPTS_FOLDER': (unicode, 'Scripts', ''),
|
||||
'SCRIPTS_ON_PLAY': (int, 'Scripts', 0),
|
||||
@@ -329,6 +357,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'SCRIPTS_ON_EXTUP': (int, 'Scripts', 0),
|
||||
'SCRIPTS_ON_INTDOWN': (int, 'Scripts', 0),
|
||||
'SCRIPTS_ON_INTUP': (int, 'Scripts', 0),
|
||||
'SCRIPTS_ON_PMSUPDATE': (int, 'Scripts', 0),
|
||||
'SCRIPTS_ON_PLAY_SCRIPT': (unicode, 'Scripts', ''),
|
||||
'SCRIPTS_ON_STOP_SCRIPT': (unicode, 'Scripts', ''),
|
||||
'SCRIPTS_ON_PAUSE_SCRIPT': (unicode, 'Scripts', ''),
|
||||
@@ -340,9 +369,11 @@ _CONFIG_DEFINITIONS = {
|
||||
'SCRIPTS_ON_EXTUP_SCRIPT': (unicode, 'Scripts', ''),
|
||||
'SCRIPTS_ON_INTDOWN_SCRIPT': (unicode, 'Scripts', ''),
|
||||
'SCRIPTS_ON_INTUP_SCRIPT': (unicode, 'Scripts', ''),
|
||||
'SCRIPTS_ON_PMSUPDATE_SCRIPT': (unicode, 'Scripts', ''),
|
||||
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
||||
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
||||
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
|
||||
'TELEGRAM_INCL_SUBJECT': (int, 'Telegram', 1),
|
||||
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
|
||||
@@ -354,15 +385,18 @@ _CONFIG_DEFINITIONS = {
|
||||
'TELEGRAM_ON_INTDOWN': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_EXTUP': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_INTUP': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_PMSUPDATE': (int, 'Telegram', 0),
|
||||
'TV_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||
'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||
'TV_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
||||
'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
||||
'TWITTER_ENABLED': (int, 'Twitter', 0),
|
||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
||||
'TWITTER_PREFIX': (str, 'Twitter', 'PlexPy'),
|
||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
||||
'TWITTER_ACCESS_TOKEN': (str, 'Twitter', ''),
|
||||
'TWITTER_ACCESS_TOKEN_SECRET': (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_STOP': (int, 'Twitter', 0),
|
||||
'TWITTER_ON_PAUSE': (int, 'Twitter', 0),
|
||||
@@ -374,6 +408,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'TWITTER_ON_INTDOWN': (int, 'Twitter', 0),
|
||||
'TWITTER_ON_EXTUP': (int, 'Twitter', 0),
|
||||
'TWITTER_ON_INTUP': (int, 'Twitter', 0),
|
||||
'TWITTER_ON_PMSUPDATE': (int, 'Twitter', 0),
|
||||
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
||||
'UPDATE_SECTION_IDS': (int, 'General', 1),
|
||||
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
||||
@@ -392,7 +427,8 @@ _CONFIG_DEFINITIONS = {
|
||||
'XBMC_ON_EXTDOWN': (int, 'XBMC', 0),
|
||||
'XBMC_ON_INTDOWN': (int, 'XBMC', 0),
|
||||
'XBMC_ON_EXTUP': (int, 'XBMC', 0),
|
||||
'XBMC_ON_INTUP': (int, 'XBMC', 0)
|
||||
'XBMC_ON_INTUP': (int, 'XBMC', 0),
|
||||
'XBMC_ON_PMSUPDATE': (int, 'XBMC', 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -511,6 +547,7 @@ class Config(object):
|
||||
self.MOVIE_LOGGING_ENABLE = 0
|
||||
self.TV_LOGGING_ENABLE = 0
|
||||
self.CONFIG_VERSION = '1'
|
||||
|
||||
if self.CONFIG_VERSION == '1':
|
||||
# Change home_stats_cards to list
|
||||
if self.HOME_STATS_CARDS:
|
||||
@@ -524,4 +561,20 @@ class Config(object):
|
||||
if 'library_statistics' in home_library_cards:
|
||||
home_library_cards.remove('library_statistics')
|
||||
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,32 +13,91 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger
|
||||
|
||||
import sqlite3
|
||||
import arrow
|
||||
import os
|
||||
import plexpy
|
||||
import time
|
||||
import sqlite3
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
|
||||
import logger
|
||||
import plexpy
|
||||
|
||||
|
||||
db_lock = threading.Lock()
|
||||
|
||||
|
||||
def drop_session_db():
|
||||
monitor_db = MonitorDatabase()
|
||||
monitor_db.action('DROP TABLE sessions')
|
||||
|
||||
|
||||
def clear_history_tables():
|
||||
logger.debug(u"PlexPy Database :: Deleting all session_history records... No turning back now bub.")
|
||||
monitor_db = MonitorDatabase()
|
||||
monitor_db.action('DELETE FROM session_history')
|
||||
monitor_db.action('DELETE FROM session_history_media_info')
|
||||
monitor_db.action('DELETE FROM session_history_metadata')
|
||||
monitor_db.action('VACUUM;')
|
||||
monitor_db.action('VACUUM')
|
||||
|
||||
|
||||
def delete_sessions():
|
||||
logger.debug(u"PlexPy Database :: Clearing temporary sessions from database.")
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
try:
|
||||
monitor_db.action('DELETE FROM sessions')
|
||||
monitor_db.action('VACUUM')
|
||||
return 'Cleared temporary sessions.'
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Database :: Unable to clear temporary sessions from database: %s." % e)
|
||||
return 'Unable to clear temporary sessions.'
|
||||
|
||||
def db_filename(filename="plexpy.db"):
|
||||
""" Returns the filepath to the db """
|
||||
|
||||
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():
|
||||
# This will protect against typecasting problems produced by empty string and None settings
|
||||
if not plexpy.CONFIG.CACHE_SIZEMB:
|
||||
@@ -46,6 +105,7 @@ def get_cache_size():
|
||||
return 0
|
||||
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
||||
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
@@ -87,15 +147,15 @@ class MonitorDatabase(object):
|
||||
|
||||
except sqlite3.OperationalError, e:
|
||||
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
|
||||
time.sleep(1)
|
||||
else:
|
||||
logger.error('Database error: %s', e)
|
||||
logger.error(u"PlexPy Database :: Database error: %s", e)
|
||||
raise
|
||||
|
||||
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
|
||||
|
||||
return sql_result
|
||||
@@ -139,7 +199,7 @@ class MonitorDatabase(object):
|
||||
try:
|
||||
self.action(insert_query, value_dict.values() + key_dict.values())
|
||||
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
|
||||
return trans_type
|
||||
|
@@ -58,11 +58,10 @@ class DataFactory(object):
|
||||
'session_history_metadata.thumb',
|
||||
'session_history_metadata.parent_thumb',
|
||||
'session_history_metadata.grandparent_thumb',
|
||||
'((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 \
|
||||
ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete',
|
||||
'session_history_media_info.video_decision',
|
||||
'session_history_media_info.audio_decision',
|
||||
'MAX((CASE WHEN (view_offset IS NULL OR view_offset = "") THEN 0.1 ELSE view_offset * 1.0 END) / \
|
||||
(CASE WHEN (session_history_metadata.duration IS NULL OR session_history_metadata.duration = "") \
|
||||
THEN 1.0 ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete',
|
||||
'session_history_media_info.transcode_decision',
|
||||
'COUNT(*) AS group_count',
|
||||
'GROUP_CONCAT(session_history.id) AS group_ids'
|
||||
]
|
||||
@@ -138,8 +137,7 @@ class DataFactory(object):
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'thumb': thumb,
|
||||
'video_decision': item['video_decision'],
|
||||
'audio_decision': item['audio_decision'],
|
||||
'transcode_decision': item['transcode_decision'],
|
||||
'percent_complete': int(round(item['percent_complete'])),
|
||||
'watched_status': watched_status,
|
||||
'group_count': item['group_count'],
|
||||
@@ -626,24 +624,21 @@ class DataFactory(object):
|
||||
|
||||
title = 'Concurrent Transcodes'
|
||||
query = base_query \
|
||||
+ 'AND (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") '
|
||||
+ 'AND session_history_media_info.transcode_decision = "transcode" '
|
||||
result = monitor_db.select(query)
|
||||
if result:
|
||||
most_concurrent.append(calc_most_concurrent(title, result))
|
||||
|
||||
title = 'Concurrent Direct Streams'
|
||||
query = base_query \
|
||||
+ 'AND (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") '
|
||||
+ 'AND session_history_media_info.transcode_decision = "copy" '
|
||||
result = monitor_db.select(query)
|
||||
if result:
|
||||
most_concurrent.append(calc_most_concurrent(title, result))
|
||||
|
||||
title = 'Concurrent Direct Plays'
|
||||
query = base_query \
|
||||
+ 'AND (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") '
|
||||
+ 'AND session_history_media_info.transcode_decision = "direct play" '
|
||||
result = monitor_db.select(query)
|
||||
if result:
|
||||
most_concurrent.append(calc_most_concurrent(title, result))
|
||||
@@ -664,7 +659,8 @@ class DataFactory(object):
|
||||
for id in library_cards:
|
||||
if id.isdigit():
|
||||
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 ' \
|
||||
'WHERE section_id = %s ' % id
|
||||
result = monitor_db.select(query)
|
||||
@@ -673,10 +669,17 @@ class DataFactory(object):
|
||||
return None
|
||||
|
||||
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'],
|
||||
'section_name': item['section_name'],
|
||||
'section_type': item['section_type'],
|
||||
'thumb': item['thumb'],
|
||||
'thumb': library_thumb,
|
||||
'count': item['count'],
|
||||
'parent_count': item['parent_count'],
|
||||
'child_count': item['child_count']
|
||||
@@ -820,6 +823,7 @@ class DataFactory(object):
|
||||
'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS total_duration ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \
|
||||
'JOIN session_history_media_info ON session_history_media_info.id = session_history.id ' \
|
||||
'%s ' % where
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
@@ -835,19 +839,67 @@ class DataFactory(object):
|
||||
def get_session_ip(self, session_key=''):
|
||||
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'
|
||||
|
||||
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:
|
||||
ip_address = item['ip_address']
|
||||
|
||||
return ip_address
|
||||
|
||||
def get_poster_url(self, rating_key='', metadata=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
poster_url = ''
|
||||
poster_key = ''
|
||||
|
||||
if rating_key:
|
||||
poster_key = rating_key
|
||||
elif metadata:
|
||||
if metadata['media_type'] == 'movie' or metadata['media_type'] == 'show' or \
|
||||
metadata['media_type'] == 'artist' or metadata['media_type'] == 'album':
|
||||
poster_key = metadata['rating_key']
|
||||
elif metadata['media_type'] == 'episode':
|
||||
poster_key = metadata['grandparent_rating_key']
|
||||
elif metadata['media_type'] == 'season' or metadata['media_type'] == 'track':
|
||||
poster_key = metadata['parent_rating_key']
|
||||
|
||||
if poster_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(poster_key), int(poster_key), int(poster_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 delete_poster_url(self, poster_url=''):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if poster_url:
|
||||
logger.info(u"PlexPy DataFactory :: Deleting poster_url %s from the notify log database." % poster_url)
|
||||
monitor_db.upsert('notify_log', {'poster_url': None}, {'poster_url': poster_url})
|
||||
return 'Deleted poster_url %s.' % poster_url
|
||||
else:
|
||||
return 'Unable to delete poster_url.'
|
||||
|
||||
def get_search_query(self, rating_key=''):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
@@ -1086,4 +1138,84 @@ class DataFactory(object):
|
||||
metadata['studio'],
|
||||
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
|
||||
|
||||
def delete_notification_log(self):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
logger.info(u"PlexPy DataFactory :: Clearing notification logs from database.")
|
||||
monitor_db.action('DELETE FROM notify_log')
|
||||
monitor_db.action('VACUUM')
|
||||
return 'Cleared notification logs.'
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy DataFactory :: Unable to execute database query for delete_notification_log: %s." % e)
|
||||
return 'Unable to clear notification logs.'
|
115
plexpy/graphs.py
115
plexpy/graphs.py
@@ -14,9 +14,9 @@
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger, database, helpers, common
|
||||
import plexpy
|
||||
|
||||
import datetime
|
||||
import locale
|
||||
|
||||
|
||||
class Graphs(object):
|
||||
@@ -321,7 +321,7 @@ class Graphs(object):
|
||||
dt = datetime.datetime(*month_item[:6])
|
||||
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_2_value = 0
|
||||
series_3_value = 0
|
||||
@@ -490,40 +490,39 @@ class Graphs(object):
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT date(session_history.started, "unixepoch", "localtime") AS date_played, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") THEN 1 ELSE 0 END) AS tc_count ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'THEN 1 ELSE 0 END) AS tc_count ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
|
||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \
|
||||
'session_history.media_type = "track") ' \
|
||||
'GROUP BY date_played ' \
|
||||
'ORDER BY started ASC' % time_range
|
||||
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT date(session_history.started, "unixepoch", "localtime") AS date_played, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
||||
'datetime("now", "-%s days", "localtime") AND ' \
|
||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
|
||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \
|
||||
'session_history.media_type = "track") ' \
|
||||
'GROUP BY date_played ' \
|
||||
'ORDER BY started ASC' % time_range
|
||||
|
||||
@@ -583,12 +582,12 @@ class Graphs(object):
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT session_history_media_info.video_resolution AS resolution, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'COUNT(session_history.id) AS total_count ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||
@@ -602,16 +601,13 @@ class Graphs(object):
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT session_history_media_info.video_resolution AS resolution,' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
|
||||
@@ -671,12 +667,12 @@ class Graphs(object):
|
||||
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
||||
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
|
||||
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" '\
|
||||
'OR session_history_media_info.audio_decision = "transcode") THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\
|
||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'COUNT(session_history.id) AS total_count ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||
@@ -700,16 +696,13 @@ class Graphs(object):
|
||||
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
||||
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
|
||||
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
|
||||
@@ -759,12 +752,12 @@ class Graphs(object):
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT session_history.platform AS platform, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'COUNT(session_history.id) AS total_count ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||
@@ -777,16 +770,13 @@ class Graphs(object):
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT session_history.platform AS platform, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "transcode") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN session_history.stopped > 0 ' \
|
||||
@@ -838,12 +828,12 @@ class Graphs(object):
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT ' \
|
||||
'(CASE WHEN users.friendly_name IS NULL THEN users.username ELSE users.friendly_name END) AS username, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'OR session_history_media_info.audio_decision = "transcode") THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||
'COUNT(session_history.id) AS total_count ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||
@@ -858,16 +848,13 @@ class Graphs(object):
|
||||
else:
|
||||
query = 'SELECT ' \
|
||||
'(CASE WHEN users.friendly_name IS NULL THEN users.username ELSE users.friendly_name END) AS username, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "direct play" ' \
|
||||
'OR session_history_media_info.audio_decision = "direct play") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision != "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "copy") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN (session_history_media_info.video_decision = "transcode" ' \
|
||||
'AND session_history_media_info.audio_decision = "transcode") ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count, ' \
|
||||
'SUM(CASE WHEN session_history.stopped > 0 ' \
|
||||
|
@@ -13,22 +13,61 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from operator import itemgetter
|
||||
from xml.dom import minidom
|
||||
|
||||
import unicodedata
|
||||
import plexpy
|
||||
import base64
|
||||
import datetime
|
||||
import fnmatch
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
from functools import wraps
|
||||
from IPy import IP
|
||||
import json
|
||||
import xmltodict
|
||||
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):
|
||||
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
|
||||
|
||||
def convert_seconds_to_minutes(s):
|
||||
|
||||
if str(s).isdigit():
|
||||
minutes = round(float(s) / 60, 0)
|
||||
|
||||
return math.trunc(minutes)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def today():
|
||||
today = datetime.date.today()
|
||||
@@ -164,7 +212,7 @@ def human_duration(s, sig='dhms'):
|
||||
if sig >= 'dh' and h > 0:
|
||||
h = h + 1 if sig == 'dh' and m >= 30 else h
|
||||
hd_list.append(str(h) + ' hrs')
|
||||
|
||||
|
||||
if sig >= 'dhm' and m > 0:
|
||||
m = m + 1 if sig == 'dhm' and s >= 30 else m
|
||||
hd_list.append(str(m) + ' mins')
|
||||
@@ -332,7 +380,7 @@ def split_string(mystring, splitvar=','):
|
||||
|
||||
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.
|
||||
|
||||
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 OpenSSL import crypto
|
||||
from certgen import createKeyPair, createCertRequest, createCertificate, \
|
||||
TYPE_RSA, serial
|
||||
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
||||
|
||||
# Create the CA Certificate
|
||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
serial = int(time.time())
|
||||
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
||||
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
|
||||
altNames = ','.join(domains + ips)
|
||||
|
||||
# Create the self-signed PlexPy certificate
|
||||
logger.debug(u"Generating self-signed SSL certificate.")
|
||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||
req = createCertRequest(pkey, CN="PlexPy")
|
||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
cert = createSelfSignedCertificate(("PlexPy", pkey), serial, (0, 60 * 60 * 24 * 365 * 10), altNames) # ten years
|
||||
|
||||
# Save the key and certificate to disk
|
||||
try:
|
||||
with open(ssl_key, "w") as fp:
|
||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(ssl_cert, "w") as fp:
|
||||
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:
|
||||
logger.error("Error creating SSL key and certificate: %s", e)
|
||||
return False
|
||||
@@ -369,14 +417,14 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
def cast_to_int(s):
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
return -1
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
def cast_to_float(s):
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return -1
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
def convert_xml_to_json(xml):
|
||||
o = xmltodict.parse(xml)
|
||||
@@ -446,4 +494,71 @@ def sanitize(string):
|
||||
if string:
|
||||
return unicode(string).replace('<','<').replace('>','>')
|
||||
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
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger, helpers
|
||||
from httplib import HTTPSConnection
|
||||
from httplib import HTTPConnection
|
||||
import ssl
|
||||
from plexpy import logger, helpers
|
||||
|
||||
|
||||
class HTTPHandler(object):
|
||||
@@ -44,7 +44,8 @@ class HTTPHandler(object):
|
||||
headers=None,
|
||||
output_format='raw',
|
||||
return_type=False,
|
||||
no_token=False):
|
||||
no_token=False,
|
||||
timeout=20):
|
||||
|
||||
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
|
||||
@@ -56,12 +57,12 @@ class HTTPHandler(object):
|
||||
if proto.upper() == 'HTTPS':
|
||||
if not self.ssl_verify and hasattr(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.")
|
||||
else:
|
||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=20)
|
||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout)
|
||||
else:
|
||||
handler = HTTPConnection(host=self.host, port=self.port, timeout=20)
|
||||
handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout)
|
||||
|
||||
token_string = ''
|
||||
if not no_token:
|
||||
|
@@ -18,62 +18,86 @@ import plexpy
|
||||
|
||||
def update_section_ids():
|
||||
from plexpy import pmsconnect, activity_pinger
|
||||
import threading
|
||||
#import threading
|
||||
|
||||
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',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
plexpy.schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
#logger.debug(u"PlexPy Libraries :: Disabling monitoring while update in progress.")
|
||||
#plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
|
||||
# hours=0, minutes=0, seconds=0)
|
||||
#plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||
# 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()
|
||||
|
||||
try:
|
||||
query = 'SELECT id, rating_key FROM session_history_metadata WHERE section_id IS NULL'
|
||||
result = monitor_db.select(query=query)
|
||||
query = 'SELECT id, rating_key, grandparent_rating_key, media_type ' \
|
||||
'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:
|
||||
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.")
|
||||
plexpy.CONFIG.__setattr__('UPDATE_SECTION_IDS', 1)
|
||||
plexpy.CONFIG.UPDATE_SECTION_IDS = 1
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
||||
plexpy.initialize_scheduler()
|
||||
#logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
||||
#plexpy.initialize_scheduler()
|
||||
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
|
||||
logger.debug(u"PlexPy Libraries :: Disabling logging in the current thread while update in progress.")
|
||||
thread_filter = logger.NoThreadFilter(threading.current_thread().name)
|
||||
for handler in logger.logger.handlers:
|
||||
handler.addFilter(thread_filter)
|
||||
#logger.debug(u"PlexPy Libraries :: Disabling logging in the current thread while update in progress.")
|
||||
#thread_filter = logger.NoThreadFilter(threading.current_thread().name)
|
||||
#for handler in logger.logger.handlers:
|
||||
# handler.addFilter(thread_filter)
|
||||
|
||||
# Get rating_key: section_id mapping pairs
|
||||
key_mappings = {}
|
||||
|
||||
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()
|
||||
for item in result:
|
||||
id = item['id']
|
||||
rating_key = item['rating_key']
|
||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
|
||||
|
||||
if metadata:
|
||||
metadata = metadata['metadata']
|
||||
section_keys = {'id': id}
|
||||
section_values = {'section_id': metadata['section_id']}
|
||||
monitor_db.upsert('session_history_metadata', key_dict=section_keys, value_dict=section_values)
|
||||
for item in history_results:
|
||||
rating_key = item['grandparent_rating_key'] if item['media_type'] != 'movie' else item['rating_key']
|
||||
section_id = key_mappings.get(str(rating_key), None)
|
||||
|
||||
if section_id:
|
||||
try:
|
||||
section_keys = {'id': item['id']}
|
||||
section_values = {'section_id': section_id}
|
||||
monitor_db.upsert('session_history_metadata', key_dict=section_keys, value_dict=section_values)
|
||||
except:
|
||||
error_keys.add(item['rating_key'])
|
||||
else:
|
||||
error_keys.add(rating_key)
|
||||
error_keys.add(item['rating_key'])
|
||||
|
||||
# Remove thread filter from the logger
|
||||
for handler in logger.logger.handlers:
|
||||
handler.removeFilter(thread_filter)
|
||||
logger.debug(u"PlexPy Libraries :: Re-enabling logging in the current thread.")
|
||||
#for handler in logger.logger.handlers:
|
||||
# handler.removeFilter(thread_filter)
|
||||
#logger.debug(u"PlexPy Libraries :: Re-enabling logging in the current thread.")
|
||||
|
||||
if error_keys:
|
||||
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:
|
||||
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()
|
||||
|
||||
logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
||||
plexpy.initialize_scheduler()
|
||||
#logger.debug(u"PlexPy Libraries :: Re-enabling monitoring.")
|
||||
#plexpy.initialize_scheduler()
|
||||
|
||||
return True
|
||||
|
||||
@@ -109,6 +133,9 @@ class Libraries(object):
|
||||
'library_sections.custom_thumb_url AS custom_thumb',
|
||||
'library_sections.art',
|
||||
'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.id) AS id',
|
||||
'session_history_metadata.full_title AS last_played',
|
||||
@@ -121,7 +148,6 @@ class Libraries(object):
|
||||
'session_history_metadata.year',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_media_info.video_decision',
|
||||
'library_sections.do_notify',
|
||||
'library_sections.do_notify_created',
|
||||
'library_sections.keep_history'
|
||||
@@ -176,6 +202,7 @@ class Libraries(object):
|
||||
'library_thumb': library_thumb,
|
||||
'library_art': item['art'],
|
||||
'plays': item['plays'],
|
||||
'duration': item['duration'],
|
||||
'last_accessed': item['last_accessed'],
|
||||
'id': item['id'],
|
||||
'last_played': item['last_played'],
|
||||
@@ -512,52 +539,27 @@ class Libraries(object):
|
||||
def get_details(self, section_id=None):
|
||||
from plexpy import pmsconnect
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
default_return = {'section_id': 0,
|
||||
'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': 1
|
||||
}
|
||||
|
||||
try:
|
||||
if section_id:
|
||||
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 not section_id:
|
||||
return default_return
|
||||
|
||||
if result:
|
||||
library_details = {}
|
||||
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
|
||||
def get_library_details(section_id=section_id):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
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:
|
||||
if section_id:
|
||||
# Refresh libraries
|
||||
pmsconnect.refresh_libraries()
|
||||
if str(section_id).isdigit():
|
||||
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 ' \
|
||||
@@ -566,12 +568,12 @@ class Libraries(object):
|
||||
result = monitor_db.select(query, args=[section_id])
|
||||
else:
|
||||
result = []
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e)
|
||||
result = []
|
||||
|
||||
library_details = {}
|
||||
if result:
|
||||
library_details = {}
|
||||
for item in result:
|
||||
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
||||
library_thumb = item['custom_thumb']
|
||||
@@ -592,22 +594,29 @@ class Libraries(object):
|
||||
'do_notify_created': item['do_notify_created'],
|
||||
'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 %s from database. Requesting library list refresh."
|
||||
% section_id)
|
||||
# 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
|
||||
|
||||
else:
|
||||
logger.warn(u"PlexPy Users :: Unable to retrieve library %s from database. Returning 'Local' library."
|
||||
% section_id)
|
||||
# If there is no library data we must return something
|
||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||
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
|
||||
}
|
||||
return default_return
|
||||
|
||||
def get_watch_time_stats(self, section_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
@@ -856,3 +865,21 @@ class Libraries(object):
|
||||
return 'Unable to delete media info table cache, section_id not valid.'
|
||||
except Exception as 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 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:
|
||||
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:
|
||||
return []
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ from pynma import pynma
|
||||
import gntp.notifier
|
||||
import oauth2 as oauth
|
||||
import pythontwitter as twitter
|
||||
import pythonfacebook as facebook
|
||||
import pythonfacebook as facebook
|
||||
|
||||
import plexpy
|
||||
from plexpy import logger, helpers, request
|
||||
@@ -58,7 +58,7 @@ AGENT_IDS = {"Growl": 0,
|
||||
"Scripts": 15,
|
||||
"Facebook": 16}
|
||||
|
||||
|
||||
|
||||
def available_notification_agents():
|
||||
agents = [{'name': 'Growl',
|
||||
'id': AGENT_IDS['Growl'],
|
||||
@@ -75,7 +75,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.GROWL_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.GROWL_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.GROWL_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.GROWL_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.GROWL_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.GROWL_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Prowl',
|
||||
'id': AGENT_IDS['Prowl'],
|
||||
@@ -92,7 +93,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.PROWL_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.PROWL_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.PROWL_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.PROWL_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.PROWL_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.PROWL_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'XBMC',
|
||||
'id': AGENT_IDS['XBMC'],
|
||||
@@ -109,7 +111,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.XBMC_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.XBMC_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.XBMC_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.XBMC_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.XBMC_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.XBMC_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Plex',
|
||||
'id': AGENT_IDS['Plex'],
|
||||
@@ -126,7 +129,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.PLEX_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.PLEX_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.PLEX_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.PLEX_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.PLEX_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.PLEX_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'NotifyMyAndroid',
|
||||
'id': AGENT_IDS['NMA'],
|
||||
@@ -143,7 +147,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.NMA_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.NMA_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.NMA_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.NMA_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.NMA_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.NMA_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Pushalot',
|
||||
'id': AGENT_IDS['Pushalot'],
|
||||
@@ -160,7 +165,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.PUSHALOT_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.PUSHALOT_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.PUSHALOT_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.PUSHALOT_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.PUSHALOT_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.PUSHALOT_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Pushbullet',
|
||||
'id': AGENT_IDS['Pushbullet'],
|
||||
@@ -177,7 +183,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.PUSHBULLET_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.PUSHBULLET_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.PUSHBULLET_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.PUSHBULLET_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.PUSHBULLET_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.PUSHBULLET_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Pushover',
|
||||
'id': AGENT_IDS['Pushover'],
|
||||
@@ -194,7 +201,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.PUSHOVER_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.PUSHOVER_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.PUSHOVER_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.PUSHOVER_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.PUSHOVER_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.PUSHOVER_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Boxcar2',
|
||||
'id': AGENT_IDS['Boxcar2'],
|
||||
@@ -211,7 +219,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.BOXCAR_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.BOXCAR_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.BOXCAR_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.BOXCAR_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.BOXCAR_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.BOXCAR_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'E-mail',
|
||||
'id': AGENT_IDS['Email'],
|
||||
@@ -228,7 +237,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.EMAIL_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.EMAIL_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.EMAIL_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.EMAIL_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.EMAIL_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.EMAIL_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Twitter',
|
||||
'id': AGENT_IDS['Twitter'],
|
||||
@@ -245,7 +255,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.TWITTER_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.TWITTER_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.TWITTER_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.TWITTER_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.TWITTER_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.TWITTER_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'IFTTT',
|
||||
'id': AGENT_IDS['IFTTT'],
|
||||
@@ -262,7 +273,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.IFTTT_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.IFTTT_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.IFTTT_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.IFTTT_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.IFTTT_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.IFTTT_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Telegram',
|
||||
'id': AGENT_IDS['Telegram'],
|
||||
@@ -279,7 +291,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.TELEGRAM_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.TELEGRAM_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.TELEGRAM_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.TELEGRAM_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.TELEGRAM_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.TELEGRAM_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Slack',
|
||||
'id': AGENT_IDS['Slack'],
|
||||
@@ -296,7 +309,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.SLACK_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.SLACK_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.SLACK_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.SLACK_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.SLACK_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.SLACK_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Scripts',
|
||||
'id': AGENT_IDS['Scripts'],
|
||||
@@ -313,7 +327,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.SCRIPTS_ON_EXTDOWN,
|
||||
'on_extup': plexpy.CONFIG.SCRIPTS_ON_EXTUP,
|
||||
'on_intdown': plexpy.CONFIG.SCRIPTS_ON_INTDOWN,
|
||||
'on_intup': plexpy.CONFIG.SCRIPTS_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.SCRIPTS_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.SCRIPTS_ON_PMSUPDATE
|
||||
},
|
||||
{'name': 'Facebook',
|
||||
'id': AGENT_IDS['Facebook'],
|
||||
@@ -330,7 +345,8 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.FACEBOOK_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.FACEBOOK_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.FACEBOOK_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.FACEBOOK_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.FACEBOOK_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.FACEBOOK_ON_PMSUPDATE
|
||||
}
|
||||
]
|
||||
|
||||
@@ -352,65 +368,66 @@ def available_notification_agents():
|
||||
'on_extdown': plexpy.CONFIG.OSX_NOTIFY_ON_EXTDOWN,
|
||||
'on_intdown': plexpy.CONFIG.OSX_NOTIFY_ON_INTDOWN,
|
||||
'on_extup': plexpy.CONFIG.OSX_NOTIFY_ON_EXTUP,
|
||||
'on_intup': plexpy.CONFIG.OSX_NOTIFY_ON_INTUP
|
||||
'on_intup': plexpy.CONFIG.OSX_NOTIFY_ON_INTUP,
|
||||
'on_pmsupdate': plexpy.CONFIG.OSX_NOTIFY_ON_PMSUPDATE
|
||||
})
|
||||
|
||||
return agents
|
||||
|
||||
|
||||
def get_notification_agent_config(config_id):
|
||||
if str(config_id).isdigit():
|
||||
config_id = int(config_id)
|
||||
def get_notification_agent_config(agent_id):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
|
||||
if config_id == 0:
|
||||
if agent_id == 0:
|
||||
growl = GROWL()
|
||||
return growl.return_config_options()
|
||||
elif config_id == 1:
|
||||
elif agent_id == 1:
|
||||
prowl = PROWL()
|
||||
return prowl.return_config_options()
|
||||
elif config_id == 2:
|
||||
elif agent_id == 2:
|
||||
xbmc = XBMC()
|
||||
return xbmc.return_config_options()
|
||||
elif config_id == 3:
|
||||
elif agent_id == 3:
|
||||
plex = Plex()
|
||||
return plex.return_config_options()
|
||||
elif config_id == 4:
|
||||
elif agent_id == 4:
|
||||
nma = NMA()
|
||||
return nma.return_config_options()
|
||||
elif config_id == 5:
|
||||
elif agent_id == 5:
|
||||
pushalot = PUSHALOT()
|
||||
return pushalot.return_config_options()
|
||||
elif config_id == 6:
|
||||
elif agent_id == 6:
|
||||
pushbullet = PUSHBULLET()
|
||||
return pushbullet.return_config_options()
|
||||
elif config_id == 7:
|
||||
elif agent_id == 7:
|
||||
pushover = PUSHOVER()
|
||||
return pushover.return_config_options()
|
||||
elif config_id == 8:
|
||||
elif agent_id == 8:
|
||||
osx_notify = OSX_NOTIFY()
|
||||
return osx_notify.return_config_options()
|
||||
elif config_id == 9:
|
||||
elif agent_id == 9:
|
||||
boxcar = BOXCAR()
|
||||
return boxcar.return_config_options()
|
||||
elif config_id == 10:
|
||||
elif agent_id == 10:
|
||||
email = Email()
|
||||
return email.return_config_options()
|
||||
elif config_id == 11:
|
||||
elif agent_id == 11:
|
||||
tweet = TwitterNotifier()
|
||||
return tweet.return_config_options()
|
||||
elif config_id == 12:
|
||||
elif agent_id == 12:
|
||||
iftttClient = IFTTT()
|
||||
return iftttClient.return_config_options()
|
||||
elif config_id == 13:
|
||||
elif agent_id == 13:
|
||||
telegramClient = TELEGRAM()
|
||||
return telegramClient.return_config_options()
|
||||
elif config_id == 14:
|
||||
elif agent_id == 14:
|
||||
slackClient = SLACK()
|
||||
return slackClient.return_config_options()
|
||||
elif config_id == 15:
|
||||
elif agent_id == 15:
|
||||
script = Scripts()
|
||||
return script.return_config_options()
|
||||
elif config_id == 16:
|
||||
elif agent_id == 16:
|
||||
facebook = FacebookNotifier()
|
||||
return facebook.return_config_options()
|
||||
else:
|
||||
@@ -419,61 +436,61 @@ def get_notification_agent_config(config_id):
|
||||
return []
|
||||
|
||||
|
||||
def send_notification(config_id, subject, body, **kwargs):
|
||||
if str(config_id).isdigit():
|
||||
config_id = int(config_id)
|
||||
def send_notification(agent_id, subject, body, **kwargs):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
|
||||
if config_id == 0:
|
||||
if agent_id == 0:
|
||||
growl = GROWL()
|
||||
growl.notify(message=body, event=subject)
|
||||
elif config_id == 1:
|
||||
elif agent_id == 1:
|
||||
prowl = PROWL()
|
||||
prowl.notify(message=body, event=subject)
|
||||
elif config_id == 2:
|
||||
elif agent_id == 2:
|
||||
xbmc = XBMC()
|
||||
xbmc.notify(subject=subject, message=body)
|
||||
elif config_id == 3:
|
||||
elif agent_id == 3:
|
||||
plex = Plex()
|
||||
plex.notify(subject=subject, message=body)
|
||||
elif config_id == 4:
|
||||
elif agent_id == 4:
|
||||
nma = NMA()
|
||||
nma.notify(subject=subject, message=body)
|
||||
elif config_id == 5:
|
||||
elif agent_id == 5:
|
||||
pushalot = PUSHALOT()
|
||||
pushalot.notify(message=body, event=subject)
|
||||
elif config_id == 6:
|
||||
elif agent_id == 6:
|
||||
pushbullet = PUSHBULLET()
|
||||
pushbullet.notify(message=body, subject=subject)
|
||||
elif config_id == 7:
|
||||
elif agent_id == 7:
|
||||
pushover = PUSHOVER()
|
||||
pushover.notify(message=body, event=subject)
|
||||
elif config_id == 8:
|
||||
elif agent_id == 8:
|
||||
osx_notify = OSX_NOTIFY()
|
||||
osx_notify.notify(title=subject, text=body)
|
||||
elif config_id == 9:
|
||||
elif agent_id == 9:
|
||||
boxcar = BOXCAR()
|
||||
boxcar.notify(title=subject, message=body)
|
||||
elif config_id == 10:
|
||||
elif agent_id == 10:
|
||||
email = Email()
|
||||
email.notify(subject=subject, message=body)
|
||||
elif config_id == 11:
|
||||
elif agent_id == 11:
|
||||
tweet = TwitterNotifier()
|
||||
tweet.notify(subject=subject, message=body)
|
||||
elif config_id == 12:
|
||||
elif agent_id == 12:
|
||||
iftttClient = IFTTT()
|
||||
iftttClient.notify(subject=subject, message=body)
|
||||
elif config_id == 13:
|
||||
elif agent_id == 13:
|
||||
telegramClient = TELEGRAM()
|
||||
telegramClient.notify(message=body, event=subject)
|
||||
elif config_id == 14:
|
||||
elif agent_id == 14:
|
||||
slackClient = SLACK()
|
||||
slackClient.notify(message=body, event=subject)
|
||||
elif config_id == 15:
|
||||
elif agent_id == 15:
|
||||
scripts = Scripts()
|
||||
scripts.notify(message=body, subject=subject, **kwargs)
|
||||
elif config_id == 16:
|
||||
elif agent_id == 16:
|
||||
facebook = FacebookNotifier()
|
||||
facebook.notify(subject=subject, message=body)
|
||||
facebook.notify(subject=subject, message=body, **kwargs)
|
||||
else:
|
||||
logger.debug(u"PlexPy Notifiers :: Unknown agent id received.")
|
||||
else:
|
||||
@@ -1054,6 +1071,7 @@ class PUSHOVER(object):
|
||||
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
|
||||
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
|
||||
self.sound = plexpy.CONFIG.PUSHOVER_SOUND
|
||||
self.html_support = plexpy.CONFIG.PUSHOVER_HTML_SUPPORT
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Pushover', options)
|
||||
@@ -1069,6 +1087,7 @@ class PUSHOVER(object):
|
||||
'title': event.encode("utf-8"),
|
||||
'message': message.encode("utf-8"),
|
||||
'sound': plexpy.CONFIG.PUSHOVER_SOUND,
|
||||
'html': plexpy.CONFIG.PUSHOVER_HTML_SUPPORT,
|
||||
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
|
||||
|
||||
http_handler.request("POST",
|
||||
@@ -1095,11 +1114,12 @@ class PUSHOVER(object):
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority, sound):
|
||||
def test(self, keys, priority, sound, html_support):
|
||||
self.enabled = True
|
||||
self.keys = keys
|
||||
self.priority = priority
|
||||
self.sound = sound
|
||||
self.html_support = html_support
|
||||
|
||||
self.notify('Main Screen Activate', 'Test Message')
|
||||
|
||||
@@ -1151,6 +1171,12 @@ class PUSHOVER(object):
|
||||
'description': 'Set the notification sound. Leave blank for the default sound.',
|
||||
'input_type': 'select',
|
||||
'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 +1191,20 @@ class TwitterNotifier(object):
|
||||
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
|
||||
|
||||
def __init__(self):
|
||||
self.consumer_key = "2LdJKXHDUwJtjYBsdwJisIOsh"
|
||||
self.consumer_secret = "QWbUcZzAIiL4zbDCIhy2EdUkV8yEEav3qMdo5y3FugxCFelWrA"
|
||||
self.access_token = plexpy.CONFIG.TWITTER_ACCESS_TOKEN
|
||||
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):
|
||||
if not subject or not message:
|
||||
return
|
||||
else:
|
||||
self._send_tweet(subject + ': ' + message)
|
||||
if self.incl_subject:
|
||||
self._send_tweet(subject + ': ' + message)
|
||||
else:
|
||||
self._send_tweet(message)
|
||||
|
||||
def test_notify(self):
|
||||
return self._send_tweet("This is a test notification from PlexPy at " + helpers.now())
|
||||
@@ -1191,16 +1223,16 @@ class TwitterNotifier(object):
|
||||
else:
|
||||
request_token = dict(parse_qsl(content))
|
||||
|
||||
plexpy.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
|
||||
plexpy.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret']
|
||||
plexpy.CONFIG.TWITTER_ACCESS_TOKEN = request_token['oauth_token']
|
||||
plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET = request_token['oauth_token_secret']
|
||||
|
||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token']
|
||||
|
||||
def _get_credentials(self, key):
|
||||
request_token = {}
|
||||
|
||||
request_token['oauth_token'] = plexpy.CONFIG.TWITTER_USERNAME
|
||||
request_token['oauth_token_secret'] = plexpy.CONFIG.TWITTER_PASSWORD
|
||||
request_token['oauth_token'] = plexpy.CONFIG.TWITTER_ACCESS_TOKEN
|
||||
request_token['oauth_token_secret'] = plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET
|
||||
request_token['oauth_callback_confirmed'] = 'true'
|
||||
|
||||
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
|
||||
@@ -1225,20 +1257,20 @@ class TwitterNotifier(object):
|
||||
else:
|
||||
# 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'])
|
||||
plexpy.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
|
||||
plexpy.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret']
|
||||
plexpy.CONFIG.TWITTER_ACCESS_TOKEN = access_token['oauth_token']
|
||||
plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET = access_token['oauth_token_secret']
|
||||
plexpy.CONFIG.write()
|
||||
return True
|
||||
|
||||
def _send_tweet(self, message=None):
|
||||
username = self.consumer_key
|
||||
password = self.consumer_secret
|
||||
access_token_key = plexpy.CONFIG.TWITTER_USERNAME
|
||||
access_token_secret = plexpy.CONFIG.TWITTER_PASSWORD
|
||||
consumer_key = self.consumer_key
|
||||
consumer_secret = self.consumer_secret
|
||||
access_token = self.access_token
|
||||
access_token_secret = self.access_token_secret
|
||||
|
||||
# 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:
|
||||
api.PostUpdate(message)
|
||||
@@ -1251,30 +1283,43 @@ class TwitterNotifier(object):
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Instructions',
|
||||
'description': 'Step 1: Click the <strong>Request Authorization</strong> button below.<br>\
|
||||
Step 2: Input the <strong>Authorization Key</strong> you received from Step 1 below.<br>\
|
||||
Step 3: Click the <strong>Verify Key</strong> button below.',
|
||||
'description': 'Step 1: Visit <a href="' + helpers.anon_url('https://apps.twitter.com') + '" target="_blank"> \
|
||||
Twitter Apps</a> to <strong>Create New App</strong>. A vaild "Website" is not required.<br>\
|
||||
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'
|
||||
},
|
||||
{'label': 'Request Authorization',
|
||||
'value': 'Request Authorization',
|
||||
'name': 'twitterStep1',
|
||||
'description': 'Request Twitter authorization. (Ensure you allow the browser pop-up).',
|
||||
'input_type': 'button'
|
||||
},
|
||||
{'label': 'Authorization Key',
|
||||
'value': '',
|
||||
'name': 'twitter_key',
|
||||
'description': 'Your Twitter authorization key.',
|
||||
{'label': 'Twitter Consumer Key',
|
||||
'value': self.consumer_key,
|
||||
'name': 'twitter_consumer_key',
|
||||
'description': 'Your Twitter consumer key.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Verify Key',
|
||||
'value': 'Verify Key',
|
||||
'name': 'twitterStep2',
|
||||
'description': 'Verify your Twitter authorization key.',
|
||||
'input_type': 'button'
|
||||
{'label': 'Twitter Consumer Secret',
|
||||
'value': self.consumer_secret,
|
||||
'name': 'twitter_consumer_secret',
|
||||
'description': 'Your Twitter consumer secret.',
|
||||
'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 with the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1597,7 +1642,8 @@ class IFTTT(object):
|
||||
config_option = [{'label': 'Ifttt Maker Channel Key',
|
||||
'value': self.apikey,
|
||||
'name': 'ifttt_key',
|
||||
'description': 'Your Ifttt key. You can get a key from <a href="https://ifttt.com/maker" target="_blank">here</a>.',
|
||||
'description': 'Your Ifttt key. You can get a key from'
|
||||
' <a href="' + helpers.anon_url('https://ifttt.com/maker') + '" target="_blank">here</a>.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Ifttt Event',
|
||||
@@ -1619,6 +1665,7 @@ class TELEGRAM(object):
|
||||
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
|
||||
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
|
||||
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
|
||||
self.incl_subject = plexpy.CONFIG.TELEGRAM_INCL_SUBJECT
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Telegram', options)
|
||||
@@ -1629,8 +1676,13 @@ class TELEGRAM(object):
|
||||
|
||||
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,
|
||||
'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
|
||||
'text': text}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/bot%s/%s" % (self.bot_token, "sendMessage"),
|
||||
@@ -1665,14 +1717,24 @@ class TELEGRAM(object):
|
||||
config_option = [{'label': 'Telegram Bot Token',
|
||||
'value': self.bot_token,
|
||||
'name': 'telegram_bot_token',
|
||||
'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="' + helpers.anon_url('https://telegram.me/BotFather') + '" target="_blank">@BotFather</a>'
|
||||
' on Telegram to get one.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Telegram Chat ID',
|
||||
{'label': 'Telegram Chat ID, Group ID, or Channel Username',
|
||||
'value': self.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="' + helpers.anon_url('https://telegram.me/myidbot') + '" target="_blank">@myidbot</a>'
|
||||
' on Telegram to get an ID.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'telegram_incl_subject',
|
||||
'description': 'Include the subject line with the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1689,6 +1751,7 @@ class SLACK(object):
|
||||
self.channel = plexpy.CONFIG.SLACK_CHANNEL
|
||||
self.username = plexpy.CONFIG.SLACK_USERNAME
|
||||
self.icon_emoji = plexpy.CONFIG.SLACK_ICON_EMOJI
|
||||
self.incl_subject = plexpy.CONFIG.SLACK_INCL_SUBJECT
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Slack', options)
|
||||
@@ -1698,7 +1761,12 @@ class SLACK(object):
|
||||
return
|
||||
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.username != '': data['username'] = self.username
|
||||
if self.icon_emoji != '':
|
||||
@@ -1736,10 +1804,10 @@ class SLACK(object):
|
||||
return self.notify('Main Screen Activate', 'Test Message')
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Slack Hook',
|
||||
config_option = [{'label': 'Slack Webhook URL',
|
||||
'value': self.slack_hook,
|
||||
'name': 'slack_hook',
|
||||
'description': 'Your Slack incoming webhook.',
|
||||
'description': 'Your Slack incoming webhook URL.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Slack Channel',
|
||||
@@ -1759,6 +1827,12 @@ class SLACK(object):
|
||||
'description': 'The icon you wish to show, use Slack emoji or image url. Leave blank for webhook integration default.',
|
||||
'name': 'slack_icon_emoji',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'slack_incl_subject',
|
||||
'description': 'Include the subject line with the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1768,7 +1842,7 @@ class SLACK(object):
|
||||
class Scripts(object):
|
||||
|
||||
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):
|
||||
return cherrypy.config['config'].get('Scripts', options)
|
||||
@@ -1798,7 +1872,7 @@ class Scripts(object):
|
||||
|
||||
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:
|
||||
subject(string, optional): Head text,
|
||||
@@ -1808,7 +1882,10 @@ class Scripts(object):
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
if script_args is None:
|
||||
script_args = []
|
||||
|
||||
if not plexpy.CONFIG.SCRIPTS_FOLDER:
|
||||
return
|
||||
|
||||
@@ -1825,14 +1902,14 @@ class Scripts(object):
|
||||
elif notify_action == 'resume':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_RESUME_SCRIPT
|
||||
|
||||
elif notify_action == 'watched':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_WATCHED_SCRIPT
|
||||
|
||||
elif notify_action == 'buffer':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_BUFFER_SCRIPT
|
||||
|
||||
elif notify_action == 'extdown':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_EXTDOWN_SCRIPT
|
||||
|
||||
elif notify_action == 'extup':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_EXTUP_SCRIPT
|
||||
elif notify_action == 'created':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_CREATED_SCRIPT
|
||||
|
||||
elif notify_action == 'intdown':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_INTDOWN_SCRIPT
|
||||
@@ -1840,11 +1917,14 @@ class Scripts(object):
|
||||
elif notify_action == 'intup':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_INTUP_SCRIPT
|
||||
|
||||
elif notify_action == 'created':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_CREATED_SCRIPT
|
||||
elif notify_action == 'extdown':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_EXTDOWN_SCRIPT
|
||||
|
||||
elif notify_action == 'watched':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_WATCHED_SCRIPT
|
||||
elif notify_action == 'extup':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_EXTUP_SCRIPT
|
||||
|
||||
elif notify_action == 'pmsupdate':
|
||||
script = plexpy.CONFIG.SCRIPTS_ON_PMSUPDATE_SCRIPT
|
||||
|
||||
else:
|
||||
# For manual scripts
|
||||
@@ -1860,14 +1940,16 @@ class Scripts(object):
|
||||
|
||||
name, ext = os.path.splitext(script)
|
||||
|
||||
if ext == '.py':
|
||||
prefix = 'python'
|
||||
elif ext == '.pyw':
|
||||
prefix = 'pythonw'
|
||||
elif ext == '.php':
|
||||
if ext == '.php':
|
||||
prefix = 'php'
|
||||
elif ext == '.pl':
|
||||
prefix = 'perl'
|
||||
elif ext == '.ps1':
|
||||
prefix = 'powershell -executionPolicy bypass -file'
|
||||
elif ext == '.py':
|
||||
prefix = 'python'
|
||||
elif ext == '.pyw':
|
||||
prefix = 'pythonw'
|
||||
elif ext == '.rb':
|
||||
prefix = 'ruby'
|
||||
else:
|
||||
@@ -1877,7 +1959,7 @@ class Scripts(object):
|
||||
script = script.encode(plexpy.SYS_ENCODING, 'ignore')
|
||||
|
||||
if prefix:
|
||||
script = [prefix, script]
|
||||
script = prefix.split() + [script]
|
||||
else:
|
||||
script = [script]
|
||||
|
||||
@@ -1984,13 +2066,6 @@ class Scripts(object):
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
},
|
||||
{'label': 'Plex Remote Access Down',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_EXTDOWN_SCRIPT,
|
||||
'name': 'scripts_on_extdown_script',
|
||||
'description': 'Choose the script for Plex remote access down.',
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
},
|
||||
{'label': 'Plex Server Down',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_INTDOWN_SCRIPT,
|
||||
'name': 'scripts_on_intdown_script',
|
||||
@@ -1998,6 +2073,20 @@ class Scripts(object):
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
},
|
||||
{'label': 'Plex Server Back Up',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_INTUP_SCRIPT,
|
||||
'name': 'scripts_on_intup_script',
|
||||
'description': 'Choose the script for Plex server back up.',
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
},
|
||||
{'label': 'Plex Remote Access Down',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_EXTDOWN_SCRIPT,
|
||||
'name': 'scripts_on_extdown_script',
|
||||
'description': 'Choose the script for Plex remote access down.',
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
},
|
||||
{'label': 'Plex Remote Access Back Up',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_EXTUP_SCRIPT,
|
||||
'name': 'scripts_on_extup_script',
|
||||
@@ -2005,10 +2094,10 @@ class Scripts(object):
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
},
|
||||
{'label': 'Plex Server Back Up',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_INTUP_SCRIPT,
|
||||
'name': 'scripts_on_intup_script',
|
||||
'description': 'Choose the script for Plex server back up.',
|
||||
{'label': 'Plex Update Available',
|
||||
'value': plexpy.CONFIG.SCRIPTS_ON_PMSUPDATE_SCRIPT,
|
||||
'name': 'scripts_on_pmsupdate_script',
|
||||
'description': 'Choose the script for Plex update available.',
|
||||
'input_type': 'select',
|
||||
'select_options': self.list_scripts()
|
||||
}
|
||||
@@ -2016,20 +2105,27 @@ class Scripts(object):
|
||||
|
||||
return config_option
|
||||
|
||||
|
||||
|
||||
class FacebookNotifier(object):
|
||||
|
||||
def __init__(self):
|
||||
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_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET
|
||||
self.group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
||||
self.incl_pmslink = plexpy.CONFIG.FACEBOOK_INCL_PMSLINK
|
||||
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:
|
||||
return
|
||||
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):
|
||||
return self._post_facebook(u"PlexPy Notifiers :: This is a test notification from PlexPy at " + helpers.now())
|
||||
@@ -2041,7 +2137,7 @@ class FacebookNotifier(object):
|
||||
|
||||
def _get_credentials(self, code):
|
||||
logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook")
|
||||
|
||||
|
||||
try:
|
||||
# Request user access token
|
||||
api = facebook.GraphAPI(version='2.5')
|
||||
@@ -2050,30 +2146,101 @@ class FacebookNotifier(object):
|
||||
app_id=self.app_id,
|
||||
app_secret=self.app_secret)
|
||||
access_token = response['access_token']
|
||||
|
||||
|
||||
# Request extended user access token
|
||||
api = facebook.GraphAPI(access_token=access_token, version='2.5')
|
||||
response = api.extend_access_token(app_id=self.app_id,
|
||||
app_secret=self.app_secret)
|
||||
access_token = response['access_token']
|
||||
|
||||
|
||||
plexpy.CONFIG.FACEBOOK_TOKEN = access_token
|
||||
plexpy.CONFIG.write()
|
||||
except Exception as e:
|
||||
logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e)
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def _post_facebook(self, message=None):
|
||||
access_token = plexpy.CONFIG.FACEBOOK_TOKEN
|
||||
group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
||||
def _post_facebook(self, message=None, **kwargs):
|
||||
if self.group_id:
|
||||
api = facebook.GraphAPI(access_token=self.access_token, version='2.5')
|
||||
|
||||
if group_id:
|
||||
api = facebook.GraphAPI(access_token=access_token, version='2.5')
|
||||
attachment = {}
|
||||
|
||||
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':
|
||||
title = metadata['title']
|
||||
subtitle = metadata['year']
|
||||
rating_key = metadata['rating_key']
|
||||
if metadata.get('imdb_url',''):
|
||||
poster_link = metadata.get('imdb_url', '')
|
||||
caption = 'View on IMDB.'
|
||||
elif metadata.get('themoviedb_url',''):
|
||||
poster_link = metadata.get('themoviedb_url', '')
|
||||
caption = 'View on The Movie Database.'
|
||||
|
||||
elif metadata['media_type'] == 'show':
|
||||
title = metadata['title']
|
||||
subtitle = metadata['year']
|
||||
rating_key = metadata['rating_key']
|
||||
if metadata.get('thetvdb_url',''):
|
||||
poster_link = metadata.get('thetvdb_url', '')
|
||||
caption = 'View on TheTVDB.'
|
||||
elif metadata.get('themoviedb_url',''):
|
||||
poster_link = metadata.get('themoviedb_url', '')
|
||||
caption = 'View on The Movie Database.'
|
||||
|
||||
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']
|
||||
if metadata.get('thetvdb_url',''):
|
||||
poster_link = metadata.get('thetvdb_url', '')
|
||||
caption = 'View on TheTVDB.'
|
||||
elif metadata.get('themoviedb_url',''):
|
||||
poster_link = metadata.get('themoviedb_url', '')
|
||||
caption = 'View on The Movie Database.'
|
||||
|
||||
elif metadata['media_type'] == 'artist':
|
||||
title = metadata['title']
|
||||
subtitle = ''
|
||||
rating_key = metadata['rating_key']
|
||||
if metadata.get('lastfm_url',''):
|
||||
poster_link = metadata.get('lastfm_url', '')
|
||||
caption = 'View on Last.fm.'
|
||||
|
||||
elif metadata['media_type'] == 'track':
|
||||
title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||
subtitle = metadata['parent_title']
|
||||
rating_key = metadata['parent_rating_key']
|
||||
if metadata.get('lastfm_url',''):
|
||||
poster_link = metadata.get('lastfm_url', '')
|
||||
caption = 'View on Last.fm.'
|
||||
|
||||
# Build Facebook post attachment
|
||||
if self.incl_pmslink:
|
||||
caption = 'View on Plex Web.'
|
||||
attachment['link'] = 'http://app.plex.tv/web/app#!/server/' + plexpy.CONFIG.PMS_IDENTIFIER + \
|
||||
'/details/%2Flibrary%2Fmetadata%2F' + rating_key
|
||||
attachment['caption'] = caption
|
||||
elif poster_link:
|
||||
attachment['link'] = poster_link
|
||||
attachment['caption'] = caption
|
||||
else:
|
||||
attachment['link'] = poster_url
|
||||
|
||||
attachment['picture'] = poster_url
|
||||
attachment['name'] = title
|
||||
attachment['description'] = subtitle
|
||||
|
||||
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.")
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Notifiers :: Error sending Facebook post: %s" % e)
|
||||
@@ -2087,13 +2254,17 @@ class FacebookNotifier(object):
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Instructions',
|
||||
'description': '<strong>Facebook notifications are currently experimental!</strong><br><br> \
|
||||
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>\
|
||||
Step 2: Go to <strong>Settings > Advanced</strong> and fill in \
|
||||
Step 1: Visit <a href="' + helpers.anon_url('https://developers.facebook.com/apps') + '" target="_blank"> \
|
||||
Facebook Developers</a> to add a new app using <strong>basic setup</strong>.<br>\
|
||||
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>\
|
||||
Step 3: Fill in the <strong>PlexPy URL</strong> below with the exact same URL from Step 2.<br>\
|
||||
Step 4: Fill in the <strong>App ID</strong> and <strong>App Secret</strong> below.<br>\
|
||||
Step 5: Click the <strong>Request Authorization</strong> button below.',
|
||||
Step 4: Go to <strong>App Review</strong> and toggle public to <strong>Yes</strong>.<br>\
|
||||
Step 5: Fill in the <strong>PlexPy URL</strong> below with the exact same URL from Step 3.<br>\
|
||||
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'
|
||||
},
|
||||
{'label': 'PlexPy URL',
|
||||
@@ -2125,6 +2296,25 @@ class FacebookNotifier(object):
|
||||
'name': 'facebook_group',
|
||||
'description': 'Your Facebook Group ID.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Poster Image',
|
||||
'value': self.incl_poster,
|
||||
'name': 'facebook_incl_poster',
|
||||
'description': 'Include a poster with the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Include Link to Plex Web',
|
||||
'value': self.incl_pmslink,
|
||||
'name': 'facebook_incl_pmslink',
|
||||
'description': 'Include a link to the media in Plex Web with the notifications.<br>'
|
||||
'If disabled, the link will go to IMDB, TVDB, TMDb, or Last.fm instead, if available.',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'facebook_incl_subject',
|
||||
'description': 'Include the subject line with the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
|
@@ -86,7 +86,7 @@ def get_real_pms_url():
|
||||
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
||||
plexpy.CONFIG.write()
|
||||
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.write()
|
||||
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
||||
@@ -383,7 +383,6 @@ class PlexTV(object):
|
||||
return []
|
||||
|
||||
plextv_resources = self.get_plextv_resources(include_https=include_https)
|
||||
server_urls = []
|
||||
|
||||
try:
|
||||
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)
|
||||
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:
|
||||
if helpers.get_xml_attr(a, 'clientIdentifier') == server_id:
|
||||
connections = a.getElementsByTagName('Connection')
|
||||
for connection in connections:
|
||||
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')
|
||||
}
|
||||
server_urls = get_connections(a)
|
||||
break
|
||||
|
||||
# Else no device match found
|
||||
if not server_urls:
|
||||
# Try to match the PMS_IP and PMS_PORT
|
||||
for a in xml_head:
|
||||
if helpers.get_xml_attr(a, 'provides') == 'server':
|
||||
connections = a.getElementsByTagName('Connection')
|
||||
|
||||
server_urls.append(server_details)
|
||||
# Else try to match the PMS_IP and PMS_PORT
|
||||
else:
|
||||
connections = a.getElementsByTagName('Connection')
|
||||
for connection in connections:
|
||||
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
|
||||
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
|
||||
for connection in connections:
|
||||
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
|
||||
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
|
||||
|
||||
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
if server_urls:
|
||||
break
|
||||
|
||||
return server_urls
|
||||
@@ -479,6 +493,16 @@ class PlexTV(object):
|
||||
connections = d.getElementsByTagName('Connection')
|
||||
|
||||
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'),
|
||||
'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'),
|
||||
'label': helpers.get_xml_attr(d, 'name'),
|
||||
|
@@ -251,7 +251,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
||||
hours=0, minutes=0, seconds=0)
|
||||
plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
plexpy.schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||
plexpy.schedule_job(activity_pinger.check_server_response, 'Check for Plex remote access',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
@@ -302,7 +302,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
||||
|
||||
# Skip line if we don't have a ratingKey to work with
|
||||
if not row['rating_key']:
|
||||
logger.error(u"PlexPy Importer :: Skipping record due to null ratingRey.")
|
||||
logger.error(u"PlexPy Importer :: Skipping record due to null ratingKey.")
|
||||
continue
|
||||
|
||||
# If the user_id no longer exists in the friends list, pull it from the xml.
|
||||
|
@@ -19,10 +19,11 @@ from urlparse import urlparse
|
||||
import plexpy
|
||||
import urllib2
|
||||
|
||||
|
||||
def get_server_friendly_name():
|
||||
logger.info(u"PlexPy Pmsconnect :: Requesting name from server...")
|
||||
server_name = PmsConnect().get_server_pref(pref='FriendlyName')
|
||||
|
||||
|
||||
# If friendly name is blank
|
||||
if not server_name:
|
||||
servers_info = PmsConnect().get_servers_info()
|
||||
@@ -30,7 +31,7 @@ def get_server_friendly_name():
|
||||
if server['machine_identifier'] == plexpy.CONFIG.PMS_IDENTIFIER:
|
||||
server_name = server['name']
|
||||
break
|
||||
|
||||
|
||||
if server_name and server_name != plexpy.CONFIG.PMS_NAME:
|
||||
plexpy.CONFIG.__setattr__('PMS_NAME', server_name)
|
||||
plexpy.CONFIG.write()
|
||||
@@ -38,17 +39,22 @@ def get_server_friendly_name():
|
||||
|
||||
return server_name
|
||||
|
||||
|
||||
def refresh_libraries():
|
||||
logger.info(u"PlexPy Pmsconnect :: Requesting libraries list refresh...")
|
||||
library_sections = PmsConnect().get_library_details()
|
||||
|
||||
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:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
library_keys = []
|
||||
|
||||
for section in library_sections:
|
||||
section_keys = {'server_id': server_id,
|
||||
'section_id': section['section_id']}
|
||||
@@ -67,15 +73,15 @@ def refresh_libraries():
|
||||
|
||||
library_keys.append(section['section_id'])
|
||||
|
||||
|
||||
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
||||
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
|
||||
import threading
|
||||
|
||||
# Start library section_id update on it's own thread
|
||||
threading.Thread(target=libraries.update_section_ids).start()
|
||||
|
||||
logger.info(u"PlexPy Pmsconnect :: Libraries list refreshed.")
|
||||
@@ -201,7 +207,7 @@ class PmsConnect(object):
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
|
||||
return request
|
||||
|
||||
def get_childrens_list(self, rating_key='', output_format=''):
|
||||
@@ -218,7 +224,7 @@ class PmsConnect(object):
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
|
||||
return request
|
||||
|
||||
def get_server_list(self, output_format=''):
|
||||
@@ -295,7 +301,7 @@ class PmsConnect(object):
|
||||
"""
|
||||
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,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
@@ -383,6 +389,38 @@ class PmsConnect(object):
|
||||
|
||||
return request
|
||||
|
||||
def put_updater(self, output_format=''):
|
||||
"""
|
||||
Refresh updater status.
|
||||
|
||||
Optional parameters: output_format { dict, json }
|
||||
|
||||
Output: array
|
||||
"""
|
||||
uri = '/updater/check?download=0'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='PUT',
|
||||
output_format=output_format)
|
||||
|
||||
return request
|
||||
|
||||
def get_updater(self, output_format=''):
|
||||
"""
|
||||
Return updater status.
|
||||
|
||||
Optional parameters: output_format { dict, json }
|
||||
|
||||
Output: array
|
||||
"""
|
||||
uri = '/updater/status'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
return request
|
||||
|
||||
def get_recently_added_details(self, section_id='', count='0'):
|
||||
"""
|
||||
Return processed and validated list of recently added items.
|
||||
@@ -830,7 +868,7 @@ class PmsConnect(object):
|
||||
metadata = self.get_metadata_details(str(child_rating_key), get_media_info)
|
||||
if metadata:
|
||||
metadata_list.append(metadata['metadata'])
|
||||
|
||||
|
||||
elif get_children and a.getElementsByTagName('Directory'):
|
||||
dir_main = a.getElementsByTagName('Directory')
|
||||
metadata_main = [d for d in dir_main if helpers.get_xml_attr(d, 'ratingKey')]
|
||||
@@ -839,7 +877,7 @@ class PmsConnect(object):
|
||||
metadata = self.get_metadata_children_details(str(child_rating_key), get_children, get_media_info)
|
||||
if metadata:
|
||||
metadata_list.extend(metadata['metadata'])
|
||||
|
||||
|
||||
output = {'metadata': metadata_list}
|
||||
return output
|
||||
|
||||
@@ -887,7 +925,7 @@ class PmsConnect(object):
|
||||
metadata['section_type'] = 'track'
|
||||
|
||||
metadata_list = {'metadata': metadata}
|
||||
|
||||
|
||||
return metadata_list
|
||||
|
||||
def get_current_activity(self):
|
||||
@@ -990,7 +1028,7 @@ class PmsConnect(object):
|
||||
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
||||
|
||||
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'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
@@ -1002,7 +1040,7 @@ class PmsConnect(object):
|
||||
'user_id': user_details['user_id'],
|
||||
'friendly_name': user_details['friendly_name'],
|
||||
'user_thumb': user_details['user_thumb'],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split('::ffff:')[-1],
|
||||
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
|
||||
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||
'machine_id': machine_id,
|
||||
@@ -1112,7 +1150,7 @@ class PmsConnect(object):
|
||||
|
||||
if helpers.get_xml_attr(session, 'type') == 'episode':
|
||||
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'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
@@ -1124,7 +1162,7 @@ class PmsConnect(object):
|
||||
'user_id': user_details['user_id'],
|
||||
'friendly_name': user_details['friendly_name'],
|
||||
'user_thumb': user_details['user_thumb'],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split('::ffff:')[-1],
|
||||
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
|
||||
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||
'machine_id': machine_id,
|
||||
@@ -1170,7 +1208,7 @@ class PmsConnect(object):
|
||||
|
||||
elif helpers.get_xml_attr(session, 'type') == 'movie':
|
||||
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'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
@@ -1182,7 +1220,7 @@ class PmsConnect(object):
|
||||
'user_id': user_details['user_id'],
|
||||
'friendly_name': user_details['friendly_name'],
|
||||
'user_thumb': user_details['user_thumb'],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split('::ffff:')[-1],
|
||||
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
|
||||
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||
'machine_id': machine_id,
|
||||
@@ -1228,7 +1266,7 @@ class PmsConnect(object):
|
||||
|
||||
elif helpers.get_xml_attr(session, 'type') == 'clip':
|
||||
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'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
@@ -1240,7 +1278,7 @@ class PmsConnect(object):
|
||||
'user_id': user_details['user_id'],
|
||||
'friendly_name': user_details['friendly_name'],
|
||||
'user_thumb': user_details['user_thumb'],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split('::ffff:')[-1],
|
||||
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
|
||||
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||
'machine_id': machine_id,
|
||||
@@ -1319,7 +1357,7 @@ class PmsConnect(object):
|
||||
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
||||
|
||||
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'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
@@ -1331,7 +1369,7 @@ class PmsConnect(object):
|
||||
'user_id': user_details['user_id'],
|
||||
'friendly_name': user_details['friendly_name'],
|
||||
'user_thumb': user_details['user_thumb'],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
|
||||
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split('::ffff:')[-1],
|
||||
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
|
||||
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||
'machine_id': machine_id,
|
||||
@@ -1404,7 +1442,7 @@ class PmsConnect(object):
|
||||
children_list = {'children_count': '0',
|
||||
'children_list': []
|
||||
}
|
||||
return parent_list
|
||||
return children_list
|
||||
|
||||
result_data = []
|
||||
|
||||
@@ -1473,7 +1511,7 @@ class PmsConnect(object):
|
||||
xml_head = identity.getElementsByTagName('MediaContainer')
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_local_server_identity: %s." % e)
|
||||
return []
|
||||
return {}
|
||||
|
||||
server_identity = {}
|
||||
for a in xml_head:
|
||||
@@ -1551,7 +1589,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(xml_head[0], 'title1'),
|
||||
'libraries_list': libraries_list
|
||||
}
|
||||
|
||||
|
||||
return output
|
||||
|
||||
def get_library_children_details(self, section_id='', section_type='', list_type='all', count='', rating_key='', get_media_info=False):
|
||||
@@ -1588,9 +1626,9 @@ class PmsConnect(object):
|
||||
sort_type = ''
|
||||
|
||||
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():
|
||||
library_data = self.get_children_list(rating_key, output_format='xml')
|
||||
library_data = self.get_children_list(str(rating_key), output_format='xml')
|
||||
else:
|
||||
logger.warn(u"PlexPy Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
|
||||
return []
|
||||
@@ -1608,15 +1646,15 @@ class PmsConnect(object):
|
||||
if a.getAttribute('size') == '0':
|
||||
logger.debug(u"PlexPy Pmsconnect :: No library data.")
|
||||
childern_list = {'library_count': '0',
|
||||
'childern_list': []
|
||||
}
|
||||
'childern_list': []
|
||||
}
|
||||
return childern_list
|
||||
|
||||
if rating_key:
|
||||
library_count = helpers.get_xml_attr(xml_head[0], 'size')
|
||||
else:
|
||||
library_count = helpers.get_xml_attr(xml_head[0], 'totalSize')
|
||||
|
||||
|
||||
# Get show/season info from xml_head
|
||||
|
||||
item_main = []
|
||||
@@ -1668,7 +1706,7 @@ class PmsConnect(object):
|
||||
output = {'library_count': library_count,
|
||||
'childern_list': childern_list
|
||||
}
|
||||
|
||||
|
||||
return output
|
||||
|
||||
def get_library_details(self):
|
||||
@@ -1689,8 +1727,8 @@ class PmsConnect(object):
|
||||
section_id = library['section_id']
|
||||
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':
|
||||
library_stats = {'section_id': library['section_id'],
|
||||
if children_list:
|
||||
library_stats = {'section_id': section_id,
|
||||
'section_name': library['section_name'],
|
||||
'section_type': section_type,
|
||||
'thumb': library['thumb'],
|
||||
@@ -1783,7 +1821,7 @@ class PmsConnect(object):
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_search_result_details: %s." % e)
|
||||
return []
|
||||
|
||||
|
||||
search_results_count = 0
|
||||
search_results_list = {'movie': [],
|
||||
'show': [],
|
||||
@@ -1801,8 +1839,8 @@ class PmsConnect(object):
|
||||
if totalSize == 0:
|
||||
logger.debug(u"PlexPy Pmsconnect :: No search results.")
|
||||
search_results_list = {'results_count': search_results_count,
|
||||
'results_list': []
|
||||
}
|
||||
'results_list': []
|
||||
}
|
||||
return search_results_list
|
||||
|
||||
for a in xml_head:
|
||||
@@ -1907,7 +1945,7 @@ class PmsConnect(object):
|
||||
if a.getAttribute('size'):
|
||||
if a.getAttribute('size') == '0':
|
||||
return {}
|
||||
|
||||
|
||||
title = helpers.get_xml_attr(a, 'title2')
|
||||
|
||||
if a.getElementsByTagName('Directory'):
|
||||
@@ -1952,34 +1990,33 @@ class PmsConnect(object):
|
||||
if child_rating_key:
|
||||
key = int(child_index)
|
||||
children.update({key: {'rating_key': int(child_rating_key)}})
|
||||
|
||||
|
||||
key = int(parent_index) if match_type == 'index' else parent_title
|
||||
parents.update({key:
|
||||
parents.update({key:
|
||||
{'rating_key': int(parent_rating_key),
|
||||
'children': children}
|
||||
})
|
||||
|
||||
|
||||
key = 0 if match_type == 'index' else title
|
||||
key_list = {key:
|
||||
{'rating_key': int(rating_key),
|
||||
'children': parents },
|
||||
'section_id': section_id,
|
||||
'library_name': library_name
|
||||
}
|
||||
|
||||
key_list = {key: {'rating_key': int(rating_key),
|
||||
'children': parents},
|
||||
'section_id': section_id,
|
||||
'library_name': library_name
|
||||
}
|
||||
|
||||
return key_list
|
||||
|
||||
def get_server_response(self):
|
||||
# Refresh Plex remote access port mapping first
|
||||
self.put_refresh_reachability()
|
||||
account_data = self.get_account(output_format='xml')
|
||||
|
||||
|
||||
try:
|
||||
xml_head = account_data.getElementsByTagName('MyPlex')
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_server_response: %s." % e)
|
||||
return None
|
||||
|
||||
|
||||
server_response = {}
|
||||
|
||||
for a in xml_head:
|
||||
@@ -1988,5 +2025,42 @@ class PmsConnect(object):
|
||||
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
|
||||
'public_port': helpers.get_xml_attr(a, 'publicPort')
|
||||
}
|
||||
|
||||
return server_response
|
||||
|
||||
return server_response
|
||||
|
||||
def get_update_staus(self):
|
||||
# Refresh the Plex updater status first
|
||||
self.put_updater()
|
||||
updater_status = self.get_updater(output_format='xml')
|
||||
|
||||
try:
|
||||
xml_head = updater_status.getElementsByTagName('MediaContainer')
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_update_staus: %s." % e)
|
||||
|
||||
# Catch the malformed XML on certain PMX version.
|
||||
# XML parser helper returns empty list if there is an error parsing XML
|
||||
if updater_status == []:
|
||||
logger.warn(u"Plex API updater XML is broken on the current PMS version. Please update your PMS manually.")
|
||||
logger.info(u"PlexPy is unable to check for Plex updates. Disabling check for Plex updates.")
|
||||
|
||||
# Disable check for Plex updates
|
||||
plexpy.CONFIG.MONITOR_PMS_UPDATES = 0
|
||||
plexpy.initialize_scheduler()
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
return {}
|
||||
|
||||
updater_info = {}
|
||||
for a in xml_head:
|
||||
if a.getElementsByTagName('Release'):
|
||||
release = a.getElementsByTagName('Release')
|
||||
for item in release:
|
||||
updater_info = {'can_install': helpers.get_xml_attr(a, 'canInstall'),
|
||||
'download_url': helpers.get_xml_attr(a, 'downloadURL'),
|
||||
'version': helpers.get_xml_attr(item, 'version'),
|
||||
'state': helpers.get_xml_attr(item, 'state'),
|
||||
'changelog': helpers.get_xml_attr(item, 'fixed')
|
||||
}
|
||||
|
||||
return updater_info
|
||||
|
114
plexpy/users.py
114
plexpy/users.py
@@ -32,6 +32,9 @@ class Users(object):
|
||||
'users.thumb AS user_thumb',
|
||||
'users.custom_avatar_url AS custom_thumb',
|
||||
'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.id) AS id',
|
||||
'session_history_metadata.full_title AS last_played',
|
||||
@@ -47,8 +50,7 @@ class Users(object):
|
||||
'session_history_metadata.year',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_media_info.video_decision',
|
||||
'session_history_media_info.audio_decision',
|
||||
'session_history_media_info.transcode_decision',
|
||||
'users.do_notify as do_notify',
|
||||
'users.keep_history as keep_history'
|
||||
]
|
||||
@@ -100,6 +102,7 @@ class Users(object):
|
||||
'friendly_name': item['friendly_name'],
|
||||
'user_thumb': user_thumb,
|
||||
'plays': item['plays'],
|
||||
'duration': item['duration'],
|
||||
'last_seen': item['last_seen'],
|
||||
'last_played': item['last_played'],
|
||||
'id': item['id'],
|
||||
@@ -113,8 +116,7 @@ class Users(object):
|
||||
'year': item['year'],
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'video_decision': item['video_decision'],
|
||||
'audio_decision': item['audio_decision'],
|
||||
'transcode_decision': item['transcode_decision'],
|
||||
'do_notify': helpers.checked(item['do_notify']),
|
||||
'keep_history': helpers.checked(item['keep_history'])
|
||||
}
|
||||
@@ -150,8 +152,7 @@ class Users(object):
|
||||
'session_history_metadata.year',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_media_info.video_decision',
|
||||
'session_history_media_info.audio_decision',
|
||||
'session_history_media_info.transcode_decision',
|
||||
'session_history.user',
|
||||
'session_history.user_id as custom_user_id',
|
||||
'(CASE WHEN users.friendly_name IS NULL THEN users.username ELSE \
|
||||
@@ -209,8 +210,7 @@ class Users(object):
|
||||
'year': item['year'],
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'video_decision': item['video_decision'],
|
||||
'audio_decision': item['audio_decision'],
|
||||
'transcode_decision': item['transcode_decision'],
|
||||
'friendly_name': item['friendly_name']
|
||||
}
|
||||
|
||||
@@ -241,61 +241,26 @@ class Users(object):
|
||||
def get_details(self, user_id=None, user=None):
|
||||
from plexpy import plextv
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
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 user_id = ? '
|
||||
result = monitor_db.select(query, args=[user_id])
|
||||
elif user:
|
||||
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 = []
|
||||
default_return = {'user_id': 0,
|
||||
'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': 1
|
||||
}
|
||||
|
||||
if result:
|
||||
user_details = {}
|
||||
for item in result:
|
||||
if item['friendly_name']:
|
||||
friendly_name = item['friendly_name']
|
||||
else:
|
||||
friendly_name = item['username']
|
||||
if not user_id and not user:
|
||||
return default_return
|
||||
|
||||
if item['custom_thumb'] and item['custom_thumb'] != item['user_thumb']:
|
||||
user_thumb = item['custom_thumb']
|
||||
elif item['user_thumb']:
|
||||
user_thumb = item['user_thumb']
|
||||
else:
|
||||
user_thumb = common.DEFAULT_USER_THUMB
|
||||
def get_user_details(user_id=user_id, user=user):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
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:
|
||||
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, ' \
|
||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
||||
'FROM users ' \
|
||||
@@ -313,8 +278,8 @@ class Users(object):
|
||||
logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e)
|
||||
result = []
|
||||
|
||||
user_details = {}
|
||||
if result:
|
||||
user_details = {}
|
||||
for item in result:
|
||||
if item['friendly_name']:
|
||||
friendly_name = item['friendly_name']
|
||||
@@ -339,21 +304,30 @@ class Users(object):
|
||||
'do_notify': item['do_notify'],
|
||||
'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 %s from database. Requesting user list refresh."
|
||||
% user_id if user_id else user)
|
||||
# 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
|
||||
|
||||
else:
|
||||
logger.warn(u"PlexPy Users :: Unable to retrieve user %s from database. Returning 'Local' user."
|
||||
% user_id if user_id else user)
|
||||
# If there is no user data we must return something
|
||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||
return {'user_id': None,
|
||||
'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
|
||||
}
|
||||
return default_return
|
||||
|
||||
def get_watch_time_stats(self, user_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_VERSION = "master"
|
||||
PLEXPY_RELEASE_VERSION = "1.3.1"
|
||||
PLEXPY_RELEASE_VERSION = "1.3.10"
|
||||
|
@@ -25,6 +25,7 @@ import websocket
|
||||
|
||||
name = 'websocket'
|
||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||
ws_reconnect = False
|
||||
|
||||
|
||||
def start_thread():
|
||||
@@ -34,31 +35,43 @@ def start_thread():
|
||||
threading.Thread(target=run).start()
|
||||
|
||||
|
||||
def reconnect():
|
||||
global ws_reconnect
|
||||
ws_reconnect = True
|
||||
|
||||
|
||||
def run():
|
||||
from websocket import create_connection
|
||||
|
||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||
plexpy.CONFIG.PMS_IP,
|
||||
plexpy.CONFIG.PMS_PORT
|
||||
)
|
||||
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
||||
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||
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)
|
||||
if plexpy.CONFIG.PMS_TOKEN:
|
||||
uri += '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN
|
||||
|
||||
global ws_reconnect
|
||||
ws_reconnect = False
|
||||
ws_connected = False
|
||||
reconnects = 0
|
||||
|
||||
# Try an open the websocket connection - if it fails after 15 retries fallback to polling
|
||||
while not ws_connected and reconnects <= 15:
|
||||
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)
|
||||
reconnects = 0
|
||||
ws_connected = True
|
||||
logger.info(u'PlexPy WebSocket :: Ready')
|
||||
logger.info(u"PlexPy WebSocket :: Ready")
|
||||
except IOError, e:
|
||||
logger.error(u'PlexPy WebSocket :: %s.' % e)
|
||||
logger.error(u"PlexPy WebSocket :: %s." % e)
|
||||
reconnects += 1
|
||||
time.sleep(5)
|
||||
|
||||
@@ -76,22 +89,30 @@ def run():
|
||||
if reconnects > 1:
|
||||
time.sleep(5)
|
||||
|
||||
logger.warn(u'PlexPy WebSocket :: Connection has closed, reconnecting...')
|
||||
logger.warn(u"PlexPy WebSocket :: Connection has closed, reconnecting...")
|
||||
try:
|
||||
ws = create_connection(uri)
|
||||
except IOError, e:
|
||||
logger.info(u'PlexPy WebSocket :: %s.' % e)
|
||||
logger.info(u"PlexPy WebSocket :: %s." % e)
|
||||
|
||||
else:
|
||||
ws.shutdown()
|
||||
ws_connected = False
|
||||
break
|
||||
|
||||
if not ws_connected:
|
||||
logger.error(u'PlexPy WebSocket :: Connection unavailable, falling back to polling.')
|
||||
# Check if we recieved a restart notification and close websocket connection cleanly
|
||||
if ws_reconnect:
|
||||
logger.info(u"PlexPy WebSocket :: Reconnecting websocket...")
|
||||
ws.shutdown()
|
||||
ws_connected = False
|
||||
start_thread()
|
||||
|
||||
if not ws_connected and not ws_reconnect:
|
||||
logger.error(u"PlexPy WebSocket :: Connection unavailable, falling back to polling.")
|
||||
plexpy.POLLING_FAILOVER = True
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
logger.debug(u'PlexPy WebSocket :: Leaving thread.')
|
||||
logger.debug(u"PlexPy WebSocket :: Leaving thread.")
|
||||
|
||||
|
||||
def receive(ws):
|
||||
@@ -119,7 +140,7 @@ def process(opcode, data):
|
||||
try:
|
||||
info = json.loads(data)
|
||||
except Exception as ex:
|
||||
logger.warn(u'PlexPy WebSocket :: Error decoding message from websocket: %s' % ex)
|
||||
logger.warn(u"PlexPy WebSocket :: Error decoding message from websocket: %s" % ex)
|
||||
logger.debug(data)
|
||||
return False
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,13 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cherrypy
|
||||
import plexpy
|
||||
|
||||
import cherrypy
|
||||
from plexpy import logger
|
||||
from plexpy.webserve import WebInterface
|
||||
import plexpy
|
||||
from plexpy.helpers import create_https_certificates
|
||||
from plexpy.webserve import WebInterface
|
||||
|
||||
|
||||
|
||||
def initialize(options):
|
||||
@@ -31,17 +32,15 @@ def initialize(options):
|
||||
https_key = options['https_key']
|
||||
|
||||
if enable_https:
|
||||
# If either the HTTPS certificate or key do not exist, try to make
|
||||
# self-signed ones.
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
|
||||
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
||||
if plexpy.CONFIG.HTTPS_CREATE_CERT and \
|
||||
(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):
|
||||
logger.warn("Unable to create certificate and key. Disabling " \
|
||||
"HTTPS")
|
||||
logger.warn("Unable to create certificate and key. Disabling HTTPS")
|
||||
enable_https = False
|
||||
|
||||
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
||||
"key.")
|
||||
logger.warn("Disabled HTTPS because of missing certificate and key.")
|
||||
enable_https = False
|
||||
|
||||
options_dict = {
|
||||
@@ -63,13 +62,17 @@ def initialize(options):
|
||||
protocol = "http"
|
||||
|
||||
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)
|
||||
|
||||
conf = {
|
||||
'/': {
|
||||
'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': {
|
||||
'tools.staticdir.on': True,
|
||||
@@ -87,15 +90,15 @@ def initialize(options):
|
||||
'tools.staticdir.on': True,
|
||||
'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': {
|
||||
'tools.staticdir.on': True,
|
||||
'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']:
|
||||
|
Reference in New Issue
Block a user