Compare commits
246 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
541d2904d3 | ||
![]() |
2080bbcbca | ||
![]() |
f4c38008a1 | ||
![]() |
2d38b15f1b | ||
![]() |
13257b9f86 | ||
![]() |
22f106b357 | ||
![]() |
352b5aadba | ||
![]() |
f86c9ea947 | ||
![]() |
2f5f0ba1e1 | ||
![]() |
6a72923182 | ||
![]() |
8ca6255ff3 | ||
![]() |
d3b3afd593 | ||
![]() |
57cb5ff6cf | ||
![]() |
39f3da6cde | ||
![]() |
5a236c9357 | ||
![]() |
4b0eab57a8 | ||
![]() |
74a232630a | ||
![]() |
71d023ab77 | ||
![]() |
786a374233 | ||
![]() |
41899872cd | ||
![]() |
076659db52 | ||
![]() |
8f665622d6 | ||
![]() |
5cc6e0b172 | ||
![]() |
bff22900cb | ||
![]() |
5e79c9fd62 | ||
![]() |
92f55c254c | ||
![]() |
39034e38f6 | ||
![]() |
3c7b9558fe | ||
![]() |
c8f7f40b46 | ||
![]() |
2a764cf190 | ||
![]() |
ba6ef4d629 | ||
![]() |
67d3505733 | ||
![]() |
252145cf58 | ||
![]() |
dbc62542ef | ||
![]() |
005829ab72 | ||
![]() |
448c8b0e8a | ||
![]() |
ff0e724ee5 | ||
![]() |
568e4a5ee8 | ||
![]() |
fbf4a524c1 | ||
![]() |
29db2e958f | ||
![]() |
cc7bcbf9d5 | ||
![]() |
98d4484e6c | ||
![]() |
0b126278f9 | ||
![]() |
cc919415bb | ||
![]() |
a3f398390c | ||
![]() |
5ae89368f1 | ||
![]() |
c8f1cb0a0a | ||
![]() |
f783b08b78 | ||
![]() |
85e0c6d3cd | ||
![]() |
2259a96058 | ||
![]() |
afed5841e7 | ||
![]() |
52361cd505 | ||
![]() |
b743cca7bc | ||
![]() |
1d01d0bff1 | ||
![]() |
c45a488962 | ||
![]() |
6731c44541 | ||
![]() |
b04ed83963 | ||
![]() |
c35b79e642 | ||
![]() |
ed8c7c1052 | ||
![]() |
498a074222 | ||
![]() |
3fe6db4d42 | ||
![]() |
6e5cd82dfb | ||
![]() |
cbf3488de9 | ||
![]() |
c72314fb71 | ||
![]() |
c4af6feb92 | ||
![]() |
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 | ||
![]() |
0ba5012464 | ||
![]() |
73ff28465d | ||
![]() |
7484d65dbb | ||
![]() |
4a120e7a54 | ||
![]() |
8d63d85821 | ||
![]() |
5cec84a802 | ||
![]() |
48da41690d | ||
![]() |
1c82241f30 | ||
![]() |
b1ea3bcd4e |
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
|
||||
|
213
CHANGELOG.md
@@ -1,22 +1,205 @@
|
||||
# Changelog
|
||||
|
||||
## v1.3.15 (2016-04-18)
|
||||
|
||||
* Fix: Slack notifications failing when using and icon URL.
|
||||
* Fix: 127.0.0.1 showing as an external IP address on the history tables.
|
||||
* Fix: Regression file sizes not shown in the media info table footer.
|
||||
* Fix: Retrieving proper PMS URL when multiple connections are published to plex.tv.
|
||||
* Fix: Some typos in the logger.
|
||||
* Fix: Some other typos in the WebUI. (Thanks @xtjoeytx)
|
||||
* Change: Optimized mobile web app icons and spash screens. (Thanks @alotufo)
|
||||
|
||||
|
||||
## v1.3.14 (2016-03-29)
|
||||
|
||||
* Fix: Regression for missing notify_action for script notifications.
|
||||
* Fix: Typo for home stats cards in the settings.
|
||||
|
||||
|
||||
## v1.3.13 (2016-03-27)
|
||||
|
||||
* Fix: Only mask strings longer than 5 characters in logs.
|
||||
|
||||
|
||||
## v1.3.12 (2016-03-27)
|
||||
|
||||
* Fix: "Check GitHub for updates" not rescheduling when toggling setting.
|
||||
* Fix: Bug where notifications would fail if metadata is not found.
|
||||
* Fix: Bug where notifications would fail if unable to upload poster to Imgur.
|
||||
* Fix: PlexPy will now start properly for different Python environment variables.
|
||||
* New: Feature requests moved to FeatHub.
|
||||
* New: Ability to specify a GitHub API token for updates (optional).
|
||||
* New: Mask out sensitive information from the logs.
|
||||
* New: New and updated Arnold quotes. (Thanks @Vilsol & @Chrisophogus)
|
||||
* New: "First" and "Last" page buttons to datatables.
|
||||
* New: Access log file from the "Help & Info" page.
|
||||
* New: CherryPy environment options (for development). (Thanks @codedecay)
|
||||
* New: PlexPy development environment (for development only).
|
||||
* Change: Facebook posts with a posters now include a summary.
|
||||
* Change: Facebook posts now use a default poster if the poster is not found or unable to upload to Imgur.
|
||||
* Change: IFTTT events can be fromatted with the {action} name.
|
||||
* Change: Logs now use ISO date format to avoid locale encoding errors. (Thanks @alshain)
|
||||
* Remove: Non-functioning Plex notification agent.
|
||||
|
||||
|
||||
## v1.3.11 (2016-03-15)
|
||||
|
||||
* Fix: Typo preventing history logging for websockets.
|
||||
|
||||
|
||||
## 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.
|
||||
* Fix: Empty brackets in titles on tables.
|
||||
* 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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
@@ -1,12 +1,45 @@
|
||||
# Contributing to PlexPy
|
||||
|
||||
## Issues
|
||||
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs, improvements or feature requests. However, we are glad to help you, and make sure the problem is not caused by PlexPy, but don't expect step-by-step answers.
|
||||
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs. 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
|
||||
|
||||
Feature requests are handled on [FeatHub](http://feathub.com/drzoidberg33/plexpy).
|
||||
|
||||
1. Search the existing requests to see if your suggestion has already been submitted.
|
||||
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
|
||||
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
|
||||
|
||||
## Pull Requests
|
||||
If you think you can contribute code to the PlexPy repository, do not hesitate to submit a pull request.
|
||||
|
53
PlexPy.py
@@ -1,4 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
#!/bin/sh
|
||||
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
|
||||
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
|
||||
''''which python2.7 >/dev/null 2>&1 && exec python2.7 "$0" "$@" # '''
|
||||
''''exec echo "Error: Python not found!" # '''
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of PlexPy.
|
||||
@@ -76,11 +81,14 @@ def main():
|
||||
'-d', '--daemon', action='store_true', help='Run as a daemon')
|
||||
parser.add_argument(
|
||||
'-p', '--port', type=int, help='Force PlexPy to run on a specified port')
|
||||
parser.add_argument(
|
||||
'--dev', action='store_true', help='Start PlexPy in the development environment')
|
||||
parser.add_argument(
|
||||
'--datadir', help='Specify a directory where to store your data files')
|
||||
parser.add_argument('--config', help='Specify a config file to use')
|
||||
parser.add_argument('--nolaunch', action='store_true',
|
||||
help='Prevent browser from launching on startup')
|
||||
parser.add_argument(
|
||||
'--config', help='Specify a config file to use')
|
||||
parser.add_argument(
|
||||
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
|
||||
parser.add_argument(
|
||||
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
||||
|
||||
@@ -95,6 +103,10 @@ def main():
|
||||
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
|
||||
verbose=plexpy.VERBOSE)
|
||||
|
||||
if args.dev:
|
||||
plexpy.DEV = True
|
||||
logger.debug(u"PlexPy is running in the dev environment.")
|
||||
|
||||
if args.daemon:
|
||||
if sys.platform == 'win32':
|
||||
sys.stderr.write(
|
||||
@@ -153,11 +165,24 @@ def main():
|
||||
# Put the database in the DATA_DIR
|
||||
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db')
|
||||
|
||||
if plexpy.DAEMON:
|
||||
plexpy.daemonize()
|
||||
|
||||
# Read config and start logging
|
||||
plexpy.initialize(config_file)
|
||||
|
||||
if plexpy.DAEMON:
|
||||
plexpy.daemonize()
|
||||
# Start the background threads
|
||||
plexpy.start()
|
||||
|
||||
# Open connection for websocket
|
||||
if plexpy.CONFIG.MONITORING_USE_WEBSOCKET:
|
||||
try:
|
||||
web_socket.start_thread()
|
||||
except:
|
||||
logger.warn(u"Websocket :: Unable to open connection.")
|
||||
# Fallback to polling
|
||||
plexpy.POLLING_FAILOVER = True
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
# Force the http port if neccessary
|
||||
if args.port:
|
||||
@@ -181,6 +206,7 @@ def main():
|
||||
'http_port': http_port,
|
||||
'http_host': plexpy.CONFIG.HTTP_HOST,
|
||||
'http_root': plexpy.CONFIG.HTTP_ROOT,
|
||||
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
|
||||
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
|
||||
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
|
||||
'https_cert': plexpy.CONFIG.HTTPS_CERT,
|
||||
@@ -190,21 +216,8 @@ def main():
|
||||
}
|
||||
webstart.initialize(web_config)
|
||||
|
||||
# Start the background threads
|
||||
plexpy.start()
|
||||
|
||||
# Open connection for websocket
|
||||
if plexpy.CONFIG.MONITORING_USE_WEBSOCKET:
|
||||
try:
|
||||
web_socket.start_thread()
|
||||
except:
|
||||
logger.warn(u"Websocket :: Unable to open connection.")
|
||||
# Fallback to polling
|
||||
plexpy.POLLING_FAILOVER = True
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
# Open webbrowser
|
||||
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch:
|
||||
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
|
||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
|
||||
plexpy.CONFIG.HTTP_ROOT)
|
||||
|
||||
|
164
README.md
@@ -4,118 +4,76 @@
|
||||
|
||||
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
|
||||
|
||||
###Support
|
||||
-----------
|
||||
* PlexPy Wiki: https://github.com/drzoidberg33/plexpy/wiki
|
||||
* 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!!
|
||||
|
||||
## 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
|
||||
|
||||
Feature requests are handled on [FeatHub](http://feathub.com/drzoidberg33/plexpy).
|
||||
|
||||
1. Search the existing requests to see if your suggestion has already been submitted.
|
||||
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
|
||||
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
|
||||
|
||||
## 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.
|
@@ -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);
|
||||
@@ -885,6 +886,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dashboard-recent-media {
|
||||
width: 100%;
|
||||
@@ -1326,8 +1328,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
|
||||
white-space: nowrap;
|
||||
margin-top: 3px;
|
||||
height: 21px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
float: right;
|
||||
}
|
||||
.star-rating .star-icon {
|
||||
width: auto;
|
||||
@@ -1718,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);
|
||||
@@ -2179,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;
|
||||
@@ -2402,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;
|
||||
}
|
||||
@@ -2650,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;
|
||||
}
|
||||
@@ -2664,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;
|
||||
}
|
||||
@@ -2714,3 +2732,61 @@ 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'))) {
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 349 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 4.4 MiB |
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 4.5 MiB |
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 4.7 MiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 4.6 MiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 137 KiB |
Before Width: | Height: | Size: 977 KiB After Width: | Height: | Size: 788 KiB |
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 167 KiB |
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 460 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 463 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 464 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 454 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
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>
|
||||
|
@@ -29,7 +29,7 @@ var hc_plays_by_day_options = {
|
||||
point: {
|
||||
events: {
|
||||
click: function () {
|
||||
selectHandler(this.category);
|
||||
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: [{}]
|
||||
};
|
@@ -39,7 +39,6 @@ function showMsg(msg,loader,timeout,ms,error) {
|
||||
}
|
||||
if (error) {
|
||||
feedback.css("background-color", "rgba(255,0,0,0.5)");
|
||||
console.log('is error');
|
||||
}
|
||||
$(feedback).html(message);
|
||||
feedback.fadeIn();
|
||||
@@ -49,12 +48,13 @@ function showMsg(msg,loader,timeout,ms,error) {
|
||||
message.fadeOut(function(){
|
||||
$(this).remove();
|
||||
feedback.fadeOut();
|
||||
feedback.css("background-color", "");
|
||||
});
|
||||
},ms);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -239,7 +242,8 @@ function isPrivateIP(ip_address) {
|
||||
// get IPv4 mapped address (xxx.xxx.xxx.xxx) from IPv6 addresss (::ffff:xxx.xxx.xxx.xxx)
|
||||
var parts = ip_address.split(":");
|
||||
var parts = parts[parts.length - 1].split('.');
|
||||
if (parts[0] === '10' ||
|
||||
if ((parts[0] === '127' && parts[1] === '0' && parts[2] === '0' && parts[3] === '1') ||
|
||||
(parts[0] === '10') ||
|
||||
(parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) ||
|
||||
(parts[0] === '192' && parts[1] === '168')) {
|
||||
return true;
|
||||
@@ -252,13 +256,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 +273,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 +395,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) {
|
||||
});
|
||||
};
|
@@ -21,7 +21,7 @@ history_table_options = {
|
||||
"infoFiltered":"<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||
"emptyTable": "No data in table"
|
||||
},
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"stateSave": true,
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
@@ -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>');
|
||||
@@ -133,20 +133,23 @@ history_table_options = {
|
||||
"data":"full_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + ' (' + rowData['year'] + ')</span>'
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + ' \
|
||||
(S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')</span>'
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
@@ -304,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();
|
||||
@@ -324,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();
|
||||
@@ -347,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();
|
||||
@@ -396,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();
|
||||
|
@@ -19,7 +19,7 @@ history_table_modal_options = {
|
||||
"infoFiltered":"",
|
||||
"emptyTable": "No data in table",
|
||||
},
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "simple_numbers",
|
||||
"stateSave": false,
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
@@ -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>');
|
||||
@@ -97,21 +97,24 @@ history_table_modal_options = {
|
||||
"data":"full_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + ' (' + rowData['year'] + ')</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + ' \
|
||||
(S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
}
|
||||
@@ -145,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();
|
||||
|
@@ -17,7 +17,7 @@ libraries_list_table_options = {
|
||||
"order": [ 2, 'asc'],
|
||||
"autoWidth": true,
|
||||
"stateSave": true,
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
@@ -133,23 +133,27 @@ libraries_list_table_options = {
|
||||
},
|
||||
{
|
||||
"targets": [8],
|
||||
"data":"last_watched",
|
||||
"data":"last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = ''
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
}
|
||||
@@ -157,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%"
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ var log_table_options = {
|
||||
"destroy": true,
|
||||
"serverSide": true,
|
||||
"processing": false,
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"order": [ 0, 'desc'],
|
||||
"pageLength": 50,
|
||||
"stateSave": true,
|
||||
|
@@ -22,8 +22,8 @@ media_info_table_options = {
|
||||
"infoFiltered":"<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||
"emptyTable": "No data in table"
|
||||
},
|
||||
"pagingType": "bootstrap",
|
||||
"stateSave": false,
|
||||
"pagingType": "full_numbers",
|
||||
"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>');
|
||||
@@ -66,11 +69,13 @@ media_info_table_options = {
|
||||
"data": "title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + ' (' + rowData['year'] + ')</span>'
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'show') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
|
||||
@@ -199,7 +204,7 @@ media_info_table_options = {
|
||||
},
|
||||
{
|
||||
"targets": [10],
|
||||
"data": "last_watched",
|
||||
"data": "last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
date = moment(cellData, "X").format(date_format);
|
||||
@@ -271,7 +276,7 @@ media_info_table_options = {
|
||||
get_file_sizes = false;
|
||||
}
|
||||
|
||||
$("#media_info_table_info").append('<span class="hidden-md hidden-sm hidden-xs"> with a total file size of ' +
|
||||
$("#media_info_table-SID-" + section_id + "_info").append('<span class="hidden-md hidden-sm hidden-xs"> with a total file size of ' +
|
||||
Math.round(settings.json.filtered_file_size / Math.pow(1024, 3)).toString() + ' GiB' +
|
||||
' (filtered from ' + Math.round(settings.json.total_file_size / Math.pow(1024, 3)).toString() + ' GiB)</span>');
|
||||
},
|
||||
@@ -289,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();
|
||||
@@ -427,7 +432,7 @@ function childTableFormatMedia(rowData) {
|
||||
'<th align="left" id="audio_codec">Audio Codec</th>' +
|
||||
'<th align="left" id="audio_channels">Audio Channels</th>' +
|
||||
'<th align="left" id="file_size">File Size</th>' +
|
||||
'<th align="left" id="last_watched">Last Watched</th>' +
|
||||
'<th align="left" id="last_played">Last Played</th>' +
|
||||
'<th align="left" id="total_plays">Total Plays</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
|
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": "full_numbers",
|
||||
"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)
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ var plex_log_table_options = {
|
||||
"destroy": true,
|
||||
"processing": false,
|
||||
"serverSide": false,
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"order": [ 0, 'desc'],
|
||||
"pageLength": 50,
|
||||
"stateSave": true,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
sync_table_options = {
|
||||
"processing": false,
|
||||
"serverSide": false,
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"order": [ [ 0, 'desc'], [ 1, 'asc'], [2, 'asc'] ],
|
||||
"pageLength": 25,
|
||||
"stateSave": true,
|
||||
|
@@ -9,7 +9,7 @@ user_ip_table_options = {
|
||||
"emptyTable": "No data in table",
|
||||
},
|
||||
"stateSave": true,
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
"pageLength": 10,
|
||||
@@ -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,12 +64,12 @@ user_ip_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData) {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_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') {
|
||||
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['video_decision'] === '') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span> ';
|
||||
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['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['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>');
|
||||
} else {
|
||||
@@ -81,23 +81,27 @@ user_ip_table_options = {
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"data":"last_watched",
|
||||
"data":"last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = ''
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
@@ -142,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();
|
||||
@@ -165,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();
|
||||
|
@@ -17,7 +17,7 @@ users_list_table_options = {
|
||||
"order": [ 2, 'asc'],
|
||||
"autoWidth": true,
|
||||
"stateSave": true,
|
||||
"pagingType": "bootstrap",
|
||||
"pagingType": "full_numbers",
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
@@ -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>');
|
||||
@@ -137,23 +137,27 @@ users_list_table_options = {
|
||||
},
|
||||
{
|
||||
"targets": [7],
|
||||
"data":"last_watched",
|
||||
"data":"last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = ''
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80" data-width="80">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
}
|
||||
@@ -161,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>
|
||||
|
||||
@@ -13,7 +14,7 @@
|
||||
</div>
|
||||
% elif config['update_section_ids'] == -1:
|
||||
<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 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.
|
||||
</div>
|
||||
@@ -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:
|
||||
@@ -46,8 +48,9 @@
|
||||
<th align="left" id="parent_count">Total Seasons / Albums</th>
|
||||
<th align="left" id="child_count">Total Episodes / Tracks</th>
|
||||
<th align="left" id="last_accessed">Last Accessed</th>
|
||||
<th align="left" id="last_watched">Last Watched</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>
|
||||
@@ -240,7 +258,7 @@ DOCUMENTATION :: END
|
||||
<th align="left" id="audio_codec">Audio Codec</th>
|
||||
<th align="left" id="audio_channels">Audio Channels</th>
|
||||
<th align="left" id="file_size">File Size</th>
|
||||
<th align="left" id="last_watched">Last Watched</th>
|
||||
<th align="left" id="last_played">Last Played</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -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,6 +32,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-added-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<li>
|
||||
@@ -90,7 +91,8 @@ DOCUMENTATION :: END
|
||||
% 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,30 +123,66 @@ 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);
|
||||
}
|
||||
|
||||
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 () {
|
||||
$("#clear-logs").hide();
|
||||
$("#clear-notify-logs").hide();
|
||||
LoadPlexLogs();
|
||||
clearSearchButton('plex_log_table', plex_log_table);
|
||||
});
|
||||
|
||||
$("#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) {
|
||||
@@ -122,6 +190,19 @@ from plexpy import helpers
|
||||
}
|
||||
});
|
||||
|
||||
$("#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) {
|
||||
@@ -143,13 +143,12 @@ from plexpy import helpers
|
||||
|
||||
$('#osxnotifyregister').click(function () {
|
||||
var osx_notify_app = $('#osx_notify_app').val();
|
||||
$.get('/osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||
})
|
||||
|
||||
$('#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;
|
||||
});
|
||||
|
||||
@@ -163,12 +162,12 @@ from plexpy import helpers
|
||||
});
|
||||
|
||||
$('#twitterStep1').click(function () {
|
||||
$.get('/twitterStep1', function (data) {window.open(data); })
|
||||
$.get('twitterStep1', 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); });
|
||||
});
|
||||
$('#twitterStep2').click(function () {
|
||||
var twitter_key = $('#twitter_key').val();
|
||||
$.get('/twitterStep2', { 'key': twitter_key }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||
$.get('twitterStep2', { 'key': twitter_key }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||
});
|
||||
|
||||
function disableFacebookRequest() {
|
||||
@@ -181,8 +180,12 @@ 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); })
|
||||
$.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,6 +32,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<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">
|
||||
@@ -83,6 +84,7 @@ DOCUMENTATION :: END
|
||||
% 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>.
|
||||
</div><br>
|
||||
|
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>
|
@@ -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,14 +148,14 @@ 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>
|
||||
<th align="left">IP Address</th>
|
||||
<th align="left">Last Platform</th>
|
||||
<th align="left">Last Player</th>
|
||||
<th align="left">Last Watched</th>
|
||||
<th align="left">Last Played</th>
|
||||
<th align="left">Play Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -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,6 +28,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-watched-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
% for item in data:
|
||||
<li>
|
||||
@@ -79,6 +80,7 @@ DOCUMENTATION :: END
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="text-muted">No stats to show.</div><br>
|
||||
% endif
|
@@ -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
|
||||
@@ -30,8 +32,9 @@
|
||||
<th align="left" id="last_known_ip">Last Known IP</th>
|
||||
<th align="left" id="last_platform">Last Platform</th>
|
||||
<th align="left" id="last_player">Last Player</th>
|
||||
<th align="left" id="last_watched">Last Watched</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>
|
||||
@@ -127,7 +127,7 @@ from plexpy import common
|
||||
</div>
|
||||
<div class="wizard-card" data-cardname="card5" data-validate="validateNotifications">
|
||||
<h3>Notifications</h3>
|
||||
<p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent conifgure this in <strong>Settings -> Notification Agents</strong>
|
||||
<p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent configure this in <strong>Settings -> Notification Agents</strong>
|
||||
after you have completed this setup wizard.</p><br/>
|
||||
<div class="wizard-input-section">
|
||||
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie playback
|
||||
@@ -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
@@ -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
8
lib/arrow/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .arrow import Arrow
|
||||
from .factory import ArrowFactory
|
||||
from .api import get, now, utcnow
|
||||
|
||||
__version__ = '0.7.0'
|
||||
VERSION = __version__
|
55
lib/arrow/api.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Provides the default implementation of :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
||||
methods for use as a module API.
|
||||
|
||||
'''
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from arrow.factory import ArrowFactory
|
||||
|
||||
|
||||
# internal default factory.
|
||||
_factory = ArrowFactory()
|
||||
|
||||
|
||||
def get(*args, **kwargs):
|
||||
''' Implements the default :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
||||
``get`` method.
|
||||
|
||||
'''
|
||||
|
||||
return _factory.get(*args, **kwargs)
|
||||
|
||||
def utcnow():
|
||||
''' Implements the default :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
||||
``utcnow`` method.
|
||||
|
||||
'''
|
||||
|
||||
return _factory.utcnow()
|
||||
|
||||
|
||||
def now(tz=None):
|
||||
''' Implements the default :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
||||
``now`` method.
|
||||
|
||||
'''
|
||||
|
||||
return _factory.now(tz)
|
||||
|
||||
|
||||
def factory(type):
|
||||
''' Returns an :class:`.ArrowFactory` for the specified :class:`Arrow <arrow.arrow.Arrow>`
|
||||
or derived type.
|
||||
|
||||
:param type: the type, :class:`Arrow <arrow.arrow.Arrow>` or derived.
|
||||
|
||||
'''
|
||||
|
||||
return ArrowFactory(type)
|
||||
|
||||
|
||||
__all__ = ['get', 'utcnow', 'now', 'factory', 'iso']
|
||||
|
896
lib/arrow/arrow.py
Normal file
@@ -0,0 +1,896 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Provides the :class:`Arrow <arrow.arrow.Arrow>` class, an enhanced ``datetime``
|
||||
replacement.
|
||||
|
||||
'''
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
from dateutil import tz as dateutil_tz
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import calendar
|
||||
import sys
|
||||
|
||||
from arrow import util, locales, parser, formatter
|
||||
|
||||
|
||||
class Arrow(object):
|
||||
'''An :class:`Arrow <arrow.arrow.Arrow>` object.
|
||||
|
||||
Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing
|
||||
additional functionality.
|
||||
|
||||
:param year: the calendar year.
|
||||
:param month: the calendar month.
|
||||
:param day: the calendar day.
|
||||
:param hour: (optional) the hour. Defaults to 0.
|
||||
:param minute: (optional) the minute, Defaults to 0.
|
||||
:param second: (optional) the second, Defaults to 0.
|
||||
:param microsecond: (optional) the microsecond. Defaults 0.
|
||||
:param tzinfo: (optional) the ``tzinfo`` object. Defaults to ``None``.
|
||||
|
||||
If tzinfo is None, it is assumed to be UTC on creation.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import arrow
|
||||
>>> arrow.Arrow(2013, 5, 5, 12, 30, 45)
|
||||
<Arrow [2013-05-05T12:30:45+00:00]>
|
||||
|
||||
'''
|
||||
|
||||
resolution = datetime.resolution
|
||||
|
||||
_ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond']
|
||||
_ATTRS_PLURAL = ['{0}s'.format(a) for a in _ATTRS]
|
||||
|
||||
def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0,
|
||||
tzinfo=None):
|
||||
|
||||
if util.isstr(tzinfo):
|
||||
tzinfo = parser.TzinfoParser.parse(tzinfo)
|
||||
tzinfo = tzinfo or dateutil_tz.tzutc()
|
||||
|
||||
self._datetime = datetime(year, month, day, hour, minute, second,
|
||||
microsecond, tzinfo)
|
||||
|
||||
|
||||
# factories: single object, both original and from datetime.
|
||||
|
||||
@classmethod
|
||||
def now(cls, tzinfo=None):
|
||||
'''Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now".
|
||||
|
||||
:param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time.
|
||||
|
||||
'''
|
||||
|
||||
utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())
|
||||
dt = utc.astimezone(dateutil_tz.tzlocal() if tzinfo is None else tzinfo)
|
||||
|
||||
return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, dt.tzinfo)
|
||||
|
||||
@classmethod
|
||||
def utcnow(cls):
|
||||
''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now" in UTC
|
||||
time.
|
||||
|
||||
'''
|
||||
|
||||
dt = datetime.utcnow()
|
||||
|
||||
return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, dateutil_tz.tzutc())
|
||||
|
||||
@classmethod
|
||||
def fromtimestamp(cls, timestamp, tzinfo=None):
|
||||
''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp.
|
||||
|
||||
:param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.
|
||||
:param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time.
|
||||
|
||||
'''
|
||||
|
||||
tzinfo = tzinfo or dateutil_tz.tzlocal()
|
||||
timestamp = cls._get_timestamp_from_input(timestamp)
|
||||
dt = datetime.fromtimestamp(timestamp, tzinfo)
|
||||
|
||||
return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, tzinfo)
|
||||
|
||||
@classmethod
|
||||
def utcfromtimestamp(cls, timestamp):
|
||||
'''Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp, in UTC time.
|
||||
|
||||
:param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.
|
||||
|
||||
'''
|
||||
|
||||
timestamp = cls._get_timestamp_from_input(timestamp)
|
||||
dt = datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, dateutil_tz.tzutc())
|
||||
|
||||
@classmethod
|
||||
def fromdatetime(cls, dt, tzinfo=None):
|
||||
''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``datetime`` and optional
|
||||
``tzinfo`` object.
|
||||
|
||||
:param dt: the ``datetime``
|
||||
:param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC.
|
||||
|
||||
'''
|
||||
|
||||
tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc()
|
||||
|
||||
return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, tzinfo)
|
||||
|
||||
@classmethod
|
||||
def fromdate(cls, date, tzinfo=None):
|
||||
''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``date`` and optional
|
||||
``tzinfo`` object. Time values are set to 0.
|
||||
|
||||
:param date: the ``date``
|
||||
:param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC.
|
||||
'''
|
||||
|
||||
tzinfo = tzinfo or dateutil_tz.tzutc()
|
||||
|
||||
return cls(date.year, date.month, date.day, tzinfo=tzinfo)
|
||||
|
||||
@classmethod
|
||||
def strptime(cls, date_str, fmt, tzinfo=None):
|
||||
''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a date string and format,
|
||||
in the style of ``datetime.strptime``.
|
||||
|
||||
:param date_str: the date string.
|
||||
:param fmt: the format string.
|
||||
:param tzinfo: (optional) an optional ``tzinfo``
|
||||
'''
|
||||
|
||||
dt = datetime.strptime(date_str, fmt)
|
||||
tzinfo = tzinfo or dt.tzinfo
|
||||
|
||||
return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, tzinfo)
|
||||
|
||||
|
||||
# factories: ranges and spans
|
||||
|
||||
@classmethod
|
||||
def range(cls, frame, start, end=None, tz=None, limit=None):
|
||||
''' Returns an array of :class:`Arrow <arrow.arrow.Arrow>` objects, representing
|
||||
an iteration of time between two inputs.
|
||||
|
||||
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
|
||||
:param start: A datetime expression, the start of the range.
|
||||
:param end: (optional) A datetime expression, the end of the range.
|
||||
:param tz: (optional) A timezone expression. Defaults to UTC.
|
||||
:param limit: (optional) A maximum number of tuples to return.
|
||||
|
||||
**NOTE**: the **end** or **limit** must be provided. Call with **end** alone to
|
||||
return the entire range, with **limit** alone to return a maximum # of results from the
|
||||
start, and with both to cap a range at a maximum # of results.
|
||||
|
||||
Supported frame values: year, quarter, month, week, day, hour, minute, second
|
||||
|
||||
Recognized datetime expressions:
|
||||
|
||||
- An :class:`Arrow <arrow.arrow.Arrow>` object.
|
||||
- A ``datetime`` object.
|
||||
|
||||
Recognized timezone expressions:
|
||||
|
||||
- A ``tzinfo`` object.
|
||||
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
|
||||
- A ``str`` in ISO-8601 style, as in '+07:00'.
|
||||
- A ``str``, one of the following: 'local', 'utc', 'UTC'.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> start = datetime(2013, 5, 5, 12, 30)
|
||||
>>> end = datetime(2013, 5, 5, 17, 15)
|
||||
>>> for r in arrow.Arrow.range('hour', start, end):
|
||||
... print repr(r)
|
||||
...
|
||||
<Arrow [2013-05-05T12:30:00+00:00]>
|
||||
<Arrow [2013-05-05T13:30:00+00:00]>
|
||||
<Arrow [2013-05-05T14:30:00+00:00]>
|
||||
<Arrow [2013-05-05T15:30:00+00:00]>
|
||||
<Arrow [2013-05-05T16:30:00+00:00]>
|
||||
|
||||
'''
|
||||
|
||||
_, frame_relative, relative_steps = cls._get_frames(frame)
|
||||
|
||||
tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)
|
||||
|
||||
start = cls._get_datetime(start).replace(tzinfo=tzinfo)
|
||||
end, limit = cls._get_iteration_params(end, limit)
|
||||
end = cls._get_datetime(end).replace(tzinfo=tzinfo)
|
||||
|
||||
current = cls.fromdatetime(start)
|
||||
results = []
|
||||
|
||||
while current <= end and len(results) < limit:
|
||||
results.append(current)
|
||||
|
||||
values = [getattr(current, f) for f in cls._ATTRS]
|
||||
current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@classmethod
|
||||
def span_range(cls, frame, start, end, tz=None, limit=None):
|
||||
''' Returns an array of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
|
||||
representing a series of timespans between two inputs.
|
||||
|
||||
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
|
||||
:param start: A datetime expression, the start of the range.
|
||||
:param end: (optional) A datetime expression, the end of the range.
|
||||
:param tz: (optional) A timezone expression. Defaults to UTC.
|
||||
:param limit: (optional) A maximum number of tuples to return.
|
||||
|
||||
**NOTE**: the **end** or **limit** must be provided. Call with **end** alone to
|
||||
return the entire range, with **limit** alone to return a maximum # of results from the
|
||||
start, and with both to cap a range at a maximum # of results.
|
||||
|
||||
Supported frame values: year, quarter, month, week, day, hour, minute, second
|
||||
|
||||
Recognized datetime expressions:
|
||||
|
||||
- An :class:`Arrow <arrow.arrow.Arrow>` object.
|
||||
- A ``datetime`` object.
|
||||
|
||||
Recognized timezone expressions:
|
||||
|
||||
- A ``tzinfo`` object.
|
||||
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
|
||||
- A ``str`` in ISO-8601 style, as in '+07:00'.
|
||||
- A ``str``, one of the following: 'local', 'utc', 'UTC'.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> start = datetime(2013, 5, 5, 12, 30)
|
||||
>>> end = datetime(2013, 5, 5, 17, 15)
|
||||
>>> for r in arrow.Arrow.span_range('hour', start, end):
|
||||
... print r
|
||||
...
|
||||
(<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T12:59:59.999999+00:00]>)
|
||||
(<Arrow [2013-05-05T13:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>)
|
||||
(<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T14:59:59.999999+00:00]>)
|
||||
(<Arrow [2013-05-05T15:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>)
|
||||
(<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T16:59:59.999999+00:00]>)
|
||||
|
||||
'''
|
||||
tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)
|
||||
start = cls.fromdatetime(start, tzinfo).span(frame)[0]
|
||||
_range = cls.range(frame, start, end, tz, limit)
|
||||
return [r.span(frame) for r in _range]
|
||||
|
||||
|
||||
# representations
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
dt = self._datetime
|
||||
attrs = ', '.join([str(i) for i in [dt.year, dt.month, dt.day, dt.hour, dt.minute,
|
||||
dt.second, dt.microsecond]])
|
||||
|
||||
return '<{0} [{1}]>'.format(self.__class__.__name__, self.__str__())
|
||||
|
||||
def __str__(self):
|
||||
return self._datetime.isoformat()
|
||||
|
||||
def __format__(self, formatstr):
|
||||
|
||||
if len(formatstr) > 0:
|
||||
return self.format(formatstr)
|
||||
|
||||
return str(self)
|
||||
|
||||
def __hash__(self):
|
||||
return self._datetime.__hash__()
|
||||
|
||||
|
||||
# attributes & properties
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
if name == 'week':
|
||||
return self.isocalendar()[1]
|
||||
|
||||
if not name.startswith('_'):
|
||||
value = getattr(self._datetime, name, None)
|
||||
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
@property
|
||||
def tzinfo(self):
|
||||
''' Gets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object. '''
|
||||
|
||||
return self._datetime.tzinfo
|
||||
|
||||
@tzinfo.setter
|
||||
def tzinfo(self, tzinfo):
|
||||
''' Sets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object. '''
|
||||
|
||||
self._datetime = self._datetime.replace(tzinfo=tzinfo)
|
||||
|
||||
@property
|
||||
def datetime(self):
|
||||
''' Returns a datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''
|
||||
|
||||
return self._datetime
|
||||
|
||||
@property
|
||||
def naive(self):
|
||||
''' Returns a naive datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''
|
||||
|
||||
return self._datetime.replace(tzinfo=None)
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
''' Returns a timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''
|
||||
|
||||
return calendar.timegm(self._datetime.utctimetuple())
|
||||
|
||||
@property
|
||||
def float_timestamp(self):
|
||||
''' Returns a floating-point representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''
|
||||
|
||||
return self.timestamp + float(self.microsecond) / 1000000
|
||||
|
||||
|
||||
# mutation and duplication.
|
||||
|
||||
def clone(self):
|
||||
''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, cloned from the current one.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> arw = arrow.utcnow()
|
||||
>>> cloned = arw.clone()
|
||||
|
||||
'''
|
||||
|
||||
return self.fromdatetime(self._datetime)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated
|
||||
according to inputs.
|
||||
|
||||
Use single property names to set their value absolutely:
|
||||
|
||||
>>> import arrow
|
||||
>>> arw = arrow.utcnow()
|
||||
>>> arw
|
||||
<Arrow [2013-05-11T22:27:34.787885+00:00]>
|
||||
>>> arw.replace(year=2014, month=6)
|
||||
<Arrow [2014-06-11T22:27:34.787885+00:00]>
|
||||
|
||||
Use plural property names to shift their current value relatively:
|
||||
|
||||
>>> arw.replace(years=1, months=-1)
|
||||
<Arrow [2014-04-11T22:27:34.787885+00:00]>
|
||||
|
||||
You can also provide a timezone expression can also be replaced:
|
||||
|
||||
>>> arw.replace(tzinfo=tz.tzlocal())
|
||||
<Arrow [2013-05-11T22:27:34.787885-07:00]>
|
||||
|
||||
Recognized timezone expressions:
|
||||
|
||||
- A ``tzinfo`` object.
|
||||
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
|
||||
- A ``str`` in ISO-8601 style, as in '+07:00'.
|
||||
- A ``str``, one of the following: 'local', 'utc', 'UTC'.
|
||||
|
||||
'''
|
||||
|
||||
absolute_kwargs = {}
|
||||
relative_kwargs = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
|
||||
if key in self._ATTRS:
|
||||
absolute_kwargs[key] = value
|
||||
elif key in self._ATTRS_PLURAL or key == 'weeks':
|
||||
relative_kwargs[key] = value
|
||||
elif key == 'week':
|
||||
raise AttributeError('setting absolute week is not supported')
|
||||
elif key !='tzinfo':
|
||||
raise AttributeError()
|
||||
|
||||
current = self._datetime.replace(**absolute_kwargs)
|
||||
current += relativedelta(**relative_kwargs)
|
||||
|
||||
tzinfo = kwargs.get('tzinfo')
|
||||
|
||||
if tzinfo is not None:
|
||||
tzinfo = self._get_tzinfo(tzinfo)
|
||||
current = current.replace(tzinfo=tzinfo)
|
||||
|
||||
return self.fromdatetime(current)
|
||||
|
||||
def to(self, tz):
|
||||
''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, converted to the target
|
||||
timezone.
|
||||
|
||||
:param tz: an expression representing a timezone.
|
||||
|
||||
Recognized timezone expressions:
|
||||
|
||||
- A ``tzinfo`` object.
|
||||
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
|
||||
- A ``str`` in ISO-8601 style, as in '+07:00'.
|
||||
- A ``str``, one of the following: 'local', 'utc', 'UTC'.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> utc = arrow.utcnow()
|
||||
>>> utc
|
||||
<Arrow [2013-05-09T03:49:12.311072+00:00]>
|
||||
|
||||
>>> utc.to('US/Pacific')
|
||||
<Arrow [2013-05-08T20:49:12.311072-07:00]>
|
||||
|
||||
>>> utc.to(tz.tzlocal())
|
||||
<Arrow [2013-05-08T20:49:12.311072-07:00]>
|
||||
|
||||
>>> utc.to('-07:00')
|
||||
<Arrow [2013-05-08T20:49:12.311072-07:00]>
|
||||
|
||||
>>> utc.to('local')
|
||||
<Arrow [2013-05-08T20:49:12.311072-07:00]>
|
||||
|
||||
>>> utc.to('local').to('utc')
|
||||
<Arrow [2013-05-09T03:49:12.311072+00:00]>
|
||||
|
||||
'''
|
||||
|
||||
if not isinstance(tz, tzinfo):
|
||||
tz = parser.TzinfoParser.parse(tz)
|
||||
|
||||
dt = self._datetime.astimezone(tz)
|
||||
|
||||
return self.__class__(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
||||
dt.microsecond, dt.tzinfo)
|
||||
|
||||
def span(self, frame, count=1):
|
||||
''' Returns two new :class:`Arrow <arrow.arrow.Arrow>` objects, representing the timespan
|
||||
of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
|
||||
|
||||
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
|
||||
:param count: (optional) the number of frames to span.
|
||||
|
||||
Supported frame values: year, quarter, month, week, day, hour, minute, second
|
||||
|
||||
Usage::
|
||||
|
||||
>>> arrow.utcnow()
|
||||
<Arrow [2013-05-09T03:32:36.186203+00:00]>
|
||||
|
||||
>>> arrow.utcnow().span('hour')
|
||||
(<Arrow [2013-05-09T03:00:00+00:00]>, <Arrow [2013-05-09T03:59:59.999999+00:00]>)
|
||||
|
||||
>>> arrow.utcnow().span('day')
|
||||
(<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-09T23:59:59.999999+00:00]>)
|
||||
|
||||
>>> arrow.utcnow().span('day', count=2)
|
||||
(<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-10T23:59:59.999999+00:00]>)
|
||||
|
||||
'''
|
||||
|
||||
frame_absolute, frame_relative, relative_steps = self._get_frames(frame)
|
||||
|
||||
if frame_absolute == 'week':
|
||||
attr = 'day'
|
||||
elif frame_absolute == 'quarter':
|
||||
attr = 'month'
|
||||
else:
|
||||
attr = frame_absolute
|
||||
|
||||
index = self._ATTRS.index(attr)
|
||||
frames = self._ATTRS[:index + 1]
|
||||
|
||||
values = [getattr(self, f) for f in frames]
|
||||
|
||||
for i in range(3 - len(values)):
|
||||
values.append(1)
|
||||
|
||||
floor = self.__class__(*values, tzinfo=self.tzinfo)
|
||||
|
||||
if frame_absolute == 'week':
|
||||
floor = floor + relativedelta(days=-(self.isoweekday() - 1))
|
||||
elif frame_absolute == 'quarter':
|
||||
floor = floor + relativedelta(months=-((self.month - 1) % 3))
|
||||
|
||||
ceil = floor + relativedelta(
|
||||
**{frame_relative: count * relative_steps}) + relativedelta(microseconds=-1)
|
||||
|
||||
return floor, ceil
|
||||
|
||||
def floor(self, frame):
|
||||
''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "floor"
|
||||
of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
|
||||
Equivalent to the first element in the 2-tuple returned by
|
||||
:func:`span <arrow.arrow.Arrow.span>`.
|
||||
|
||||
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
|
||||
|
||||
Usage::
|
||||
|
||||
>>> arrow.utcnow().floor('hour')
|
||||
<Arrow [2013-05-09T03:00:00+00:00]>
|
||||
'''
|
||||
|
||||
return self.span(frame)[0]
|
||||
|
||||
def ceil(self, frame):
|
||||
''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "ceiling"
|
||||
of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
|
||||
Equivalent to the second element in the 2-tuple returned by
|
||||
:func:`span <arrow.arrow.Arrow.span>`.
|
||||
|
||||
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
|
||||
|
||||
Usage::
|
||||
|
||||
>>> arrow.utcnow().ceil('hour')
|
||||
<Arrow [2013-05-09T03:59:59.999999+00:00]>
|
||||
'''
|
||||
|
||||
return self.span(frame)[1]
|
||||
|
||||
|
||||
# string output and formatting.
|
||||
|
||||
def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'):
|
||||
''' Returns a string representation of the :class:`Arrow <arrow.arrow.Arrow>` object,
|
||||
formatted according to a format string.
|
||||
|
||||
:param fmt: the format string.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ')
|
||||
'2013-05-09 03:56:47 -00:00'
|
||||
|
||||
>>> arrow.utcnow().format('X')
|
||||
'1368071882'
|
||||
|
||||
>>> arrow.utcnow().format('MMMM DD, YYYY')
|
||||
'May 09, 2013'
|
||||
|
||||
>>> arrow.utcnow().format()
|
||||
'2013-05-09 03:56:47 -00:00'
|
||||
|
||||
'''
|
||||
|
||||
return formatter.DateTimeFormatter(locale).format(self._datetime, fmt)
|
||||
|
||||
|
||||
def humanize(self, other=None, locale='en_us', only_distance=False):
|
||||
''' Returns a localized, humanized representation of a relative difference in time.
|
||||
|
||||
:param other: (optional) an :class:`Arrow <arrow.arrow.Arrow>` or ``datetime`` object.
|
||||
Defaults to now in the current :class:`Arrow <arrow.arrow.Arrow>` object's timezone.
|
||||
:param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'.
|
||||
:param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part.
|
||||
Usage::
|
||||
|
||||
>>> earlier = arrow.utcnow().replace(hours=-2)
|
||||
>>> earlier.humanize()
|
||||
'2 hours ago'
|
||||
|
||||
>>> later = later = earlier.replace(hours=4)
|
||||
>>> later.humanize(earlier)
|
||||
'in 4 hours'
|
||||
|
||||
'''
|
||||
|
||||
locale = locales.get_locale(locale)
|
||||
|
||||
if other is None:
|
||||
utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())
|
||||
dt = utc.astimezone(self._datetime.tzinfo)
|
||||
|
||||
elif isinstance(other, Arrow):
|
||||
dt = other._datetime
|
||||
|
||||
elif isinstance(other, datetime):
|
||||
if other.tzinfo is None:
|
||||
dt = other.replace(tzinfo=self._datetime.tzinfo)
|
||||
else:
|
||||
dt = other.astimezone(self._datetime.tzinfo)
|
||||
|
||||
else:
|
||||
raise TypeError()
|
||||
|
||||
delta = int(util.total_seconds(self._datetime - dt))
|
||||
sign = -1 if delta < 0 else 1
|
||||
diff = abs(delta)
|
||||
delta = diff
|
||||
|
||||
if diff < 10:
|
||||
return locale.describe('now', only_distance=only_distance)
|
||||
|
||||
if diff < 45:
|
||||
return locale.describe('seconds', sign, only_distance=only_distance)
|
||||
|
||||
elif diff < 90:
|
||||
return locale.describe('minute', sign, only_distance=only_distance)
|
||||
elif diff < 2700:
|
||||
minutes = sign * int(max(delta / 60, 2))
|
||||
return locale.describe('minutes', minutes, only_distance=only_distance)
|
||||
|
||||
elif diff < 5400:
|
||||
return locale.describe('hour', sign, only_distance=only_distance)
|
||||
elif diff < 79200:
|
||||
hours = sign * int(max(delta / 3600, 2))
|
||||
return locale.describe('hours', hours, only_distance=only_distance)
|
||||
|
||||
elif diff < 129600:
|
||||
return locale.describe('day', sign, only_distance=only_distance)
|
||||
elif diff < 2160000:
|
||||
days = sign * int(max(delta / 86400, 2))
|
||||
return locale.describe('days', days, only_distance=only_distance)
|
||||
|
||||
elif diff < 3888000:
|
||||
return locale.describe('month', sign, only_distance=only_distance)
|
||||
elif diff < 29808000:
|
||||
self_months = self._datetime.year * 12 + self._datetime.month
|
||||
other_months = dt.year * 12 + dt.month
|
||||
months = sign * abs(other_months - self_months)
|
||||
|
||||
return locale.describe('months', months, only_distance=only_distance)
|
||||
|
||||
elif diff < 47260800:
|
||||
return locale.describe('year', sign, only_distance=only_distance)
|
||||
else:
|
||||
years = sign * int(max(delta / 31536000, 2))
|
||||
return locale.describe('years', years, only_distance=only_distance)
|
||||
|
||||
|
||||
# math
|
||||
|
||||
def __add__(self, other):
|
||||
|
||||
if isinstance(other, (timedelta, relativedelta)):
|
||||
return self.fromdatetime(self._datetime + other, self._datetime.tzinfo)
|
||||
|
||||
raise TypeError()
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__add__(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
|
||||
if isinstance(other, timedelta):
|
||||
return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)
|
||||
|
||||
elif isinstance(other, datetime):
|
||||
return self._datetime - other
|
||||
|
||||
elif isinstance(other, Arrow):
|
||||
return self._datetime - other._datetime
|
||||
|
||||
raise TypeError()
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__sub__(other)
|
||||
|
||||
|
||||
# comparisons
|
||||
|
||||
def _cmperror(self, other):
|
||||
raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format(
|
||||
type(self), type(other)))
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
if not isinstance(other, (Arrow, datetime)):
|
||||
return False
|
||||
|
||||
other = self._get_datetime(other)
|
||||
|
||||
return self._datetime == self._get_datetime(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __gt__(self, other):
|
||||
|
||||
if not isinstance(other, (Arrow, datetime)):
|
||||
self._cmperror(other)
|
||||
|
||||
return self._datetime > self._get_datetime(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
|
||||
if not isinstance(other, (Arrow, datetime)):
|
||||
self._cmperror(other)
|
||||
|
||||
return self._datetime >= self._get_datetime(other)
|
||||
|
||||
def __lt__(self, other):
|
||||
|
||||
if not isinstance(other, (Arrow, datetime)):
|
||||
self._cmperror(other)
|
||||
|
||||
return self._datetime < self._get_datetime(other)
|
||||
|
||||
def __le__(self, other):
|
||||
|
||||
if not isinstance(other, (Arrow, datetime)):
|
||||
self._cmperror(other)
|
||||
|
||||
return self._datetime <= self._get_datetime(other)
|
||||
|
||||
|
||||
# datetime methods
|
||||
|
||||
def date(self):
|
||||
''' Returns a ``date`` object with the same year, month and day. '''
|
||||
|
||||
return self._datetime.date()
|
||||
|
||||
def time(self):
|
||||
''' Returns a ``time`` object with the same hour, minute, second, microsecond. '''
|
||||
|
||||
return self._datetime.time()
|
||||
|
||||
def timetz(self):
|
||||
''' Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. '''
|
||||
|
||||
return self._datetime.timetz()
|
||||
|
||||
def astimezone(self, tz):
|
||||
''' Returns a ``datetime`` object, adjusted to the specified tzinfo.
|
||||
|
||||
:param tz: a ``tzinfo`` object.
|
||||
|
||||
'''
|
||||
|
||||
return self._datetime.astimezone(tz)
|
||||
|
||||
def utcoffset(self):
|
||||
''' Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. '''
|
||||
|
||||
return self._datetime.utcoffset()
|
||||
|
||||
def dst(self):
|
||||
''' Returns the daylight savings time adjustment. '''
|
||||
return self._datetime.dst()
|
||||
|
||||
def timetuple(self):
|
||||
''' Returns a ``time.struct_time``, in the current timezone. '''
|
||||
|
||||
return self._datetime.timetuple()
|
||||
|
||||
def utctimetuple(self):
|
||||
''' Returns a ``time.struct_time``, in UTC time. '''
|
||||
|
||||
return self._datetime.utctimetuple()
|
||||
|
||||
def toordinal(self):
|
||||
''' Returns the proleptic Gregorian ordinal of the date. '''
|
||||
|
||||
return self._datetime.toordinal()
|
||||
|
||||
def weekday(self):
|
||||
''' Returns the day of the week as an integer (0-6). '''
|
||||
|
||||
return self._datetime.weekday()
|
||||
|
||||
def isoweekday(self):
|
||||
''' Returns the ISO day of the week as an integer (1-7). '''
|
||||
|
||||
return self._datetime.isoweekday()
|
||||
|
||||
def isocalendar(self):
|
||||
''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). '''
|
||||
|
||||
return self._datetime.isocalendar()
|
||||
|
||||
def isoformat(self, sep='T'):
|
||||
'''Returns an ISO 8601 formatted representation of the date and time. '''
|
||||
|
||||
return self._datetime.isoformat(sep)
|
||||
|
||||
def ctime(self):
|
||||
''' Returns a ctime formatted representation of the date and time. '''
|
||||
|
||||
return self._datetime.ctime()
|
||||
|
||||
def strftime(self, format):
|
||||
''' Formats in the style of ``datetime.strptime``.
|
||||
|
||||
:param format: the format string.
|
||||
|
||||
'''
|
||||
|
||||
return self._datetime.strftime(format)
|
||||
|
||||
def for_json(self):
|
||||
'''Serializes for the ``for_json`` protocol of simplejson.'''
|
||||
return self.isoformat()
|
||||
|
||||
# internal tools.
|
||||
|
||||
@staticmethod
|
||||
def _get_tzinfo(tz_expr):
|
||||
|
||||
if tz_expr is None:
|
||||
return dateutil_tz.tzutc()
|
||||
if isinstance(tz_expr, tzinfo):
|
||||
return tz_expr
|
||||
else:
|
||||
try:
|
||||
return parser.TzinfoParser.parse(tz_expr)
|
||||
except parser.ParserError:
|
||||
raise ValueError('\'{0}\' not recognized as a timezone'.format(
|
||||
tz_expr))
|
||||
|
||||
@classmethod
|
||||
def _get_datetime(cls, expr):
|
||||
|
||||
if isinstance(expr, Arrow):
|
||||
return expr.datetime
|
||||
|
||||
if isinstance(expr, datetime):
|
||||
return expr
|
||||
|
||||
try:
|
||||
expr = float(expr)
|
||||
return cls.utcfromtimestamp(expr).datetime
|
||||
except:
|
||||
raise ValueError(
|
||||
'\'{0}\' not recognized as a timestamp or datetime'.format(expr))
|
||||
|
||||
@classmethod
|
||||
def _get_frames(cls, name):
|
||||
|
||||
if name in cls._ATTRS:
|
||||
return name, '{0}s'.format(name), 1
|
||||
|
||||
elif name in ['week', 'weeks']:
|
||||
return 'week', 'weeks', 1
|
||||
elif name in ['quarter', 'quarters']:
|
||||
return 'quarter', 'months', 3
|
||||
|
||||
raise AttributeError()
|
||||
|
||||
@classmethod
|
||||
def _get_iteration_params(cls, end, limit):
|
||||
|
||||
if end is None:
|
||||
|
||||
if limit is None:
|
||||
raise Exception('one of \'end\' or \'limit\' is required')
|
||||
|
||||
return cls.max, limit
|
||||
|
||||
else:
|
||||
return end, sys.maxsize
|
||||
|
||||
@staticmethod
|
||||
def _get_timestamp_from_input(timestamp):
|
||||
|
||||
try:
|
||||
return float(timestamp)
|
||||
except:
|
||||
raise ValueError('cannot parse \'{0}\' as a timestamp'.format(timestamp))
|
||||
|
||||
Arrow.min = Arrow.fromdatetime(datetime.min)
|
||||
Arrow.max = Arrow.fromdatetime(datetime.max)
|
254
lib/arrow/factory.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Implements the :class:`ArrowFactory <arrow.factory.ArrowFactory>` class,
|
||||
providing factory methods for common :class:`Arrow <arrow.arrow.Arrow>`
|
||||
construction scenarios.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from arrow.arrow import Arrow
|
||||
from arrow import parser
|
||||
from arrow.util import is_timestamp, isstr
|
||||
|
||||
from datetime import datetime, tzinfo, date
|
||||
from dateutil import tz as dateutil_tz
|
||||
from time import struct_time
|
||||
import calendar
|
||||
|
||||
|
||||
class ArrowFactory(object):
|
||||
''' A factory for generating :class:`Arrow <arrow.arrow.Arrow>` objects.
|
||||
|
||||
:param type: (optional) the :class:`Arrow <arrow.arrow.Arrow>`-based class to construct from.
|
||||
Defaults to :class:`Arrow <arrow.arrow.Arrow>`.
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, type=Arrow):
|
||||
self.type = type
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
''' Returns an :class:`Arrow <arrow.arrow.Arrow>` object based on flexible inputs.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import arrow
|
||||
|
||||
**No inputs** to get current UTC time::
|
||||
|
||||
>>> arrow.get()
|
||||
<Arrow [2013-05-08T05:51:43.316458+00:00]>
|
||||
|
||||
**None** to also get current UTC time::
|
||||
|
||||
>>> arrow.get(None)
|
||||
<Arrow [2013-05-08T05:51:43.316458+00:00]>
|
||||
|
||||
**One** :class:`Arrow <arrow.arrow.Arrow>` object, to get a copy.
|
||||
|
||||
>>> arw = arrow.utcnow()
|
||||
>>> arrow.get(arw)
|
||||
<Arrow [2013-10-23T15:21:54.354846+00:00]>
|
||||
|
||||
**One** ``str``, ``float``, or ``int``, convertible to a floating-point timestamp, to get that timestamp in UTC::
|
||||
|
||||
>>> arrow.get(1367992474.293378)
|
||||
<Arrow [2013-05-08T05:54:34.293378+00:00]>
|
||||
|
||||
>>> arrow.get(1367992474)
|
||||
<Arrow [2013-05-08T05:54:34+00:00]>
|
||||
|
||||
>>> arrow.get('1367992474.293378')
|
||||
<Arrow [2013-05-08T05:54:34.293378+00:00]>
|
||||
|
||||
>>> arrow.get('1367992474')
|
||||
<Arrow [2013-05-08T05:54:34+0struct_time0:00]>
|
||||
|
||||
**One** ISO-8601-formatted ``str``, to parse it::
|
||||
|
||||
>>> arrow.get('2013-09-29T01:26:43.830580')
|
||||
<Arrow [2013-09-29T01:26:43.830580+00:00]>
|
||||
|
||||
**One** ``tzinfo``, to get the current time in that timezone::
|
||||
|
||||
>>> arrow.get(tz.tzlocal())
|
||||
<Arrow [2013-05-07T22:57:28.484717-07:00]>
|
||||
|
||||
**One** naive ``datetime``, to get that datetime in UTC::
|
||||
|
||||
>>> arrow.get(datetime(2013, 5, 5))
|
||||
<Arrow [2013-05-05T00:00:00+00:00]>
|
||||
|
||||
**One** aware ``datetime``, to get that datetime::
|
||||
|
||||
>>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal()))
|
||||
<Arrow [2013-05-05T00:00:00-07:00]>
|
||||
|
||||
**One** naive ``date``, to get that date in UTC::
|
||||
|
||||
>>> arrow.get(date(2013, 5, 5))
|
||||
<Arrow [2013-05-05T00:00:00+00:00]>
|
||||
|
||||
**Two** arguments, a naive or aware ``datetime``, and a timezone expression (as above)::
|
||||
|
||||
>>> arrow.get(datetime(2013, 5, 5), 'US/Pacific')
|
||||
<Arrow [2013-05-05T00:00:00-07:00]>
|
||||
|
||||
**Two** arguments, a naive ``date``, and a timezone expression (as above)::
|
||||
|
||||
>>> arrow.get(date(2013, 5, 5), 'US/Pacific')
|
||||
<Arrow [2013-05-05T00:00:00-07:00]>
|
||||
|
||||
**Two** arguments, both ``str``, to parse the first according to the format of the second::
|
||||
|
||||
>>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss')
|
||||
<Arrow [2013-05-05T12:30:45+00:00]>
|
||||
|
||||
**Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try::
|
||||
|
||||
>>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss'])
|
||||
<Arrow [2013-05-05T12:30:45+00:00]>
|
||||
|
||||
**Three or more** arguments, as for the constructor of a ``datetime``::
|
||||
|
||||
>>> arrow.get(2013, 5, 5, 12, 30, 45)
|
||||
<Arrow [2013-05-05T12:30:45+00:00]>
|
||||
|
||||
**One** time.struct time::
|
||||
>>> arrow.get(gmtime(0))
|
||||
<Arrow [1970-01-01T00:00:00+00:00]>
|
||||
|
||||
'''
|
||||
|
||||
arg_count = len(args)
|
||||
locale = kwargs.get('locale', 'en_us')
|
||||
tz = kwargs.get('tzinfo', None)
|
||||
|
||||
# () -> now, @ utc.
|
||||
if arg_count == 0:
|
||||
if isinstance(tz, tzinfo):
|
||||
return self.type.now(tz)
|
||||
return self.type.utcnow()
|
||||
|
||||
if arg_count == 1:
|
||||
arg = args[0]
|
||||
|
||||
# (None) -> now, @ utc.
|
||||
if arg is None:
|
||||
return self.type.utcnow()
|
||||
|
||||
# try (int, float, str(int), str(float)) -> utc, from timestamp.
|
||||
if is_timestamp(arg):
|
||||
return self.type.utcfromtimestamp(arg)
|
||||
|
||||
# (Arrow) -> from the object's datetime.
|
||||
if isinstance(arg, Arrow):
|
||||
return self.type.fromdatetime(arg.datetime)
|
||||
|
||||
# (datetime) -> from datetime.
|
||||
if isinstance(arg, datetime):
|
||||
return self.type.fromdatetime(arg)
|
||||
|
||||
# (date) -> from date.
|
||||
if isinstance(arg, date):
|
||||
return self.type.fromdate(arg)
|
||||
|
||||
# (tzinfo) -> now, @ tzinfo.
|
||||
elif isinstance(arg, tzinfo):
|
||||
return self.type.now(arg)
|
||||
|
||||
# (str) -> now, @ tzinfo.
|
||||
elif isstr(arg):
|
||||
dt = parser.DateTimeParser(locale).parse_iso(arg)
|
||||
return self.type.fromdatetime(dt)
|
||||
|
||||
# (struct_time) -> from struct_time
|
||||
elif isinstance(arg, struct_time):
|
||||
return self.type.utcfromtimestamp(calendar.timegm(arg))
|
||||
|
||||
else:
|
||||
raise TypeError('Can\'t parse single argument type of \'{0}\''.format(type(arg)))
|
||||
|
||||
elif arg_count == 2:
|
||||
|
||||
arg_1, arg_2 = args[0], args[1]
|
||||
|
||||
if isinstance(arg_1, datetime):
|
||||
|
||||
# (datetime, tzinfo) -> fromdatetime @ tzinfo/string.
|
||||
if isinstance(arg_2, tzinfo) or isstr(arg_2):
|
||||
return self.type.fromdatetime(arg_1, arg_2)
|
||||
else:
|
||||
raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{0}\''.format(
|
||||
type(arg_2)))
|
||||
|
||||
# (date, tzinfo/str) -> fromdate @ tzinfo/string.
|
||||
elif isinstance(arg_1, date):
|
||||
|
||||
if isinstance(arg_2, tzinfo) or isstr(arg_2):
|
||||
return self.type.fromdate(arg_1, tzinfo=arg_2)
|
||||
else:
|
||||
raise TypeError('Can\'t parse two arguments of types \'date\', \'{0}\''.format(
|
||||
type(arg_2)))
|
||||
|
||||
# (str, format) -> parse.
|
||||
elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)):
|
||||
dt = parser.DateTimeParser(locale).parse(args[0], args[1])
|
||||
return self.type.fromdatetime(dt, tzinfo=tz)
|
||||
|
||||
else:
|
||||
raise TypeError('Can\'t parse two arguments of types \'{0}\', \'{1}\''.format(
|
||||
type(arg_1), type(arg_2)))
|
||||
|
||||
# 3+ args -> datetime-like via constructor.
|
||||
else:
|
||||
return self.type(*args, **kwargs)
|
||||
|
||||
def utcnow(self):
|
||||
'''Returns an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now" in UTC time.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import arrow
|
||||
>>> arrow.utcnow()
|
||||
<Arrow [2013-05-08T05:19:07.018993+00:00]>
|
||||
'''
|
||||
|
||||
return self.type.utcnow()
|
||||
|
||||
def now(self, tz=None):
|
||||
'''Returns an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now".
|
||||
|
||||
:param tz: (optional) An expression representing a timezone. Defaults to local time.
|
||||
|
||||
Recognized timezone expressions:
|
||||
|
||||
- A ``tzinfo`` object.
|
||||
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
|
||||
- A ``str`` in ISO-8601 style, as in '+07:00'.
|
||||
- A ``str``, one of the following: 'local', 'utc', 'UTC'.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import arrow
|
||||
>>> arrow.now()
|
||||
<Arrow [2013-05-07T22:19:11.363410-07:00]>
|
||||
|
||||
>>> arrow.now('US/Pacific')
|
||||
<Arrow [2013-05-07T22:19:15.251821-07:00]>
|
||||
|
||||
>>> arrow.now('+02:00')
|
||||
<Arrow [2013-05-08T07:19:25.618646+02:00]>
|
||||
|
||||
>>> arrow.now('local')
|
||||
<Arrow [2013-05-07T22:19:39.130059-07:00]>
|
||||
'''
|
||||
|
||||
if tz is None:
|
||||
tz = dateutil_tz.tzlocal()
|
||||
elif not isinstance(tz, tzinfo):
|
||||
tz = parser.TzinfoParser.parse(tz)
|
||||
|
||||
return self.type.now(tz)
|
105
lib/arrow/formatter.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
import calendar
|
||||
import re
|
||||
from dateutil import tz as dateutil_tz
|
||||
from arrow import util, locales
|
||||
|
||||
|
||||
class DateTimeFormatter(object):
|
||||
|
||||
_FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?|a|A|X)')
|
||||
|
||||
def __init__(self, locale='en_us'):
|
||||
|
||||
self.locale = locales.get_locale(locale)
|
||||
|
||||
def format(cls, dt, fmt):
|
||||
|
||||
return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt)
|
||||
|
||||
def _format_token(self, dt, token):
|
||||
|
||||
if token == 'YYYY':
|
||||
return self.locale.year_full(dt.year)
|
||||
if token == 'YY':
|
||||
return self.locale.year_abbreviation(dt.year)
|
||||
|
||||
if token == 'MMMM':
|
||||
return self.locale.month_name(dt.month)
|
||||
if token == 'MMM':
|
||||
return self.locale.month_abbreviation(dt.month)
|
||||
if token == 'MM':
|
||||
return '{0:02d}'.format(dt.month)
|
||||
if token == 'M':
|
||||
return str(dt.month)
|
||||
|
||||
if token == 'DDDD':
|
||||
return '{0:03d}'.format(dt.timetuple().tm_yday)
|
||||
if token == 'DDD':
|
||||
return str(dt.timetuple().tm_yday)
|
||||
if token == 'DD':
|
||||
return '{0:02d}'.format(dt.day)
|
||||
if token == 'D':
|
||||
return str(dt.day)
|
||||
|
||||
if token == 'Do':
|
||||
return self.locale.ordinal_number(dt.day)
|
||||
|
||||
if token == 'dddd':
|
||||
return self.locale.day_name(dt.isoweekday())
|
||||
if token == 'ddd':
|
||||
return self.locale.day_abbreviation(dt.isoweekday())
|
||||
if token == 'd':
|
||||
return str(dt.isoweekday())
|
||||
|
||||
if token == 'HH':
|
||||
return '{0:02d}'.format(dt.hour)
|
||||
if token == 'H':
|
||||
return str(dt.hour)
|
||||
if token == 'hh':
|
||||
return '{0:02d}'.format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12))
|
||||
if token == 'h':
|
||||
return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12))
|
||||
|
||||
if token == 'mm':
|
||||
return '{0:02d}'.format(dt.minute)
|
||||
if token == 'm':
|
||||
return str(dt.minute)
|
||||
|
||||
if token == 'ss':
|
||||
return '{0:02d}'.format(dt.second)
|
||||
if token == 's':
|
||||
return str(dt.second)
|
||||
|
||||
if token == 'SSSSSS':
|
||||
return str('{0:06d}'.format(int(dt.microsecond)))
|
||||
if token == 'SSSSS':
|
||||
return str('{0:05d}'.format(int(dt.microsecond / 10)))
|
||||
if token == 'SSSS':
|
||||
return str('{0:04d}'.format(int(dt.microsecond / 100)))
|
||||
if token == 'SSS':
|
||||
return str('{0:03d}'.format(int(dt.microsecond / 1000)))
|
||||
if token == 'SS':
|
||||
return str('{0:02d}'.format(int(dt.microsecond / 10000)))
|
||||
if token == 'S':
|
||||
return str(int(dt.microsecond / 100000))
|
||||
|
||||
if token == 'X':
|
||||
return str(calendar.timegm(dt.utctimetuple()))
|
||||
|
||||
if token in ['ZZ', 'Z']:
|
||||
separator = ':' if token == 'ZZ' else ''
|
||||
tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo
|
||||
total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60)
|
||||
|
||||
sign = '+' if total_minutes > 0 else '-'
|
||||
total_minutes = abs(total_minutes)
|
||||
hour, minute = divmod(total_minutes, 60)
|
||||
|
||||
return '{0}{1:02d}{2}{3:02d}'.format(sign, hour, separator, minute)
|
||||
|
||||
if token in ('a', 'A'):
|
||||
return self.locale.meridian(dt.hour, token)
|
||||
|
1703
lib/arrow/locales.py
Normal file
308
lib/arrow/parser.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil import tz
|
||||
import re
|
||||
|
||||
from arrow import locales
|
||||
|
||||
|
||||
class ParserError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class DateTimeParser(object):
|
||||
|
||||
_FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)')
|
||||
|
||||
_ONE_THROUGH_SIX_DIGIT_RE = re.compile('\d{1,6}')
|
||||
_ONE_THROUGH_FIVE_DIGIT_RE = re.compile('\d{1,5}')
|
||||
_ONE_THROUGH_FOUR_DIGIT_RE = re.compile('\d{1,4}')
|
||||
_ONE_TWO_OR_THREE_DIGIT_RE = re.compile('\d{1,3}')
|
||||
_ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}')
|
||||
_FOUR_DIGIT_RE = re.compile('\d{4}')
|
||||
_TWO_DIGIT_RE = re.compile('\d{2}')
|
||||
_TZ_RE = re.compile('[+\-]?\d{2}:?\d{2}')
|
||||
_TZ_NAME_RE = re.compile('\w[\w+\-/]+')
|
||||
|
||||
|
||||
_BASE_INPUT_RE_MAP = {
|
||||
'YYYY': _FOUR_DIGIT_RE,
|
||||
'YY': _TWO_DIGIT_RE,
|
||||
'MM': _TWO_DIGIT_RE,
|
||||
'M': _ONE_OR_TWO_DIGIT_RE,
|
||||
'DD': _TWO_DIGIT_RE,
|
||||
'D': _ONE_OR_TWO_DIGIT_RE,
|
||||
'HH': _TWO_DIGIT_RE,
|
||||
'H': _ONE_OR_TWO_DIGIT_RE,
|
||||
'hh': _TWO_DIGIT_RE,
|
||||
'h': _ONE_OR_TWO_DIGIT_RE,
|
||||
'mm': _TWO_DIGIT_RE,
|
||||
'm': _ONE_OR_TWO_DIGIT_RE,
|
||||
'ss': _TWO_DIGIT_RE,
|
||||
's': _ONE_OR_TWO_DIGIT_RE,
|
||||
'X': re.compile('\d+'),
|
||||
'ZZZ': _TZ_NAME_RE,
|
||||
'ZZ': _TZ_RE,
|
||||
'Z': _TZ_RE,
|
||||
'SSSSSS': _ONE_THROUGH_SIX_DIGIT_RE,
|
||||
'SSSSS': _ONE_THROUGH_FIVE_DIGIT_RE,
|
||||
'SSSS': _ONE_THROUGH_FOUR_DIGIT_RE,
|
||||
'SSS': _ONE_TWO_OR_THREE_DIGIT_RE,
|
||||
'SS': _ONE_OR_TWO_DIGIT_RE,
|
||||
'S': re.compile('\d'),
|
||||
}
|
||||
|
||||
MARKERS = ['YYYY', 'MM', 'DD']
|
||||
SEPARATORS = ['-', '/', '.']
|
||||
|
||||
def __init__(self, locale='en_us'):
|
||||
|
||||
self.locale = locales.get_locale(locale)
|
||||
self._input_re_map = self._BASE_INPUT_RE_MAP.copy()
|
||||
self._input_re_map.update({
|
||||
'MMMM': self._choice_re(self.locale.month_names[1:], re.IGNORECASE),
|
||||
'MMM': self._choice_re(self.locale.month_abbreviations[1:],
|
||||
re.IGNORECASE),
|
||||
'Do': re.compile(self.locale.ordinal_day_re),
|
||||
'a': self._choice_re(
|
||||
(self.locale.meridians['am'], self.locale.meridians['pm'])
|
||||
),
|
||||
# note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to
|
||||
# ensure backwards compatibility of this token
|
||||
'A': self._choice_re(self.locale.meridians.values())
|
||||
})
|
||||
|
||||
def parse_iso(self, string):
|
||||
|
||||
has_time = 'T' in string or ' ' in string.strip()
|
||||
space_divider = ' ' in string.strip()
|
||||
|
||||
if has_time:
|
||||
if space_divider:
|
||||
date_string, time_string = string.split(' ', 1)
|
||||
else:
|
||||
date_string, time_string = string.split('T', 1)
|
||||
time_parts = re.split('[+-]', time_string, 1)
|
||||
has_tz = len(time_parts) > 1
|
||||
has_seconds = time_parts[0].count(':') > 1
|
||||
has_subseconds = '.' in time_parts[0]
|
||||
|
||||
if has_subseconds:
|
||||
subseconds_token = 'S' * min(len(re.split('\D+', time_parts[0].split('.')[1], 1)[0]), 6)
|
||||
formats = ['YYYY-MM-DDTHH:mm:ss.%s' % subseconds_token]
|
||||
elif has_seconds:
|
||||
formats = ['YYYY-MM-DDTHH:mm:ss']
|
||||
else:
|
||||
formats = ['YYYY-MM-DDTHH:mm']
|
||||
else:
|
||||
has_tz = False
|
||||
# generate required formats: YYYY-MM-DD, YYYY-MM-DD, YYYY
|
||||
# using various separators: -, /, .
|
||||
l = len(self.MARKERS)
|
||||
formats = [separator.join(self.MARKERS[:l-i])
|
||||
for i in range(l)
|
||||
for separator in self.SEPARATORS]
|
||||
|
||||
if has_time and has_tz:
|
||||
formats = [f + 'Z' for f in formats]
|
||||
|
||||
if space_divider:
|
||||
formats = [item.replace('T', ' ', 1) for item in formats]
|
||||
|
||||
return self._parse_multiformat(string, formats)
|
||||
|
||||
def parse(self, string, fmt):
|
||||
|
||||
if isinstance(fmt, list):
|
||||
return self._parse_multiformat(string, fmt)
|
||||
|
||||
# fmt is a string of tokens like 'YYYY-MM-DD'
|
||||
# we construct a new string by replacing each
|
||||
# token by its pattern:
|
||||
# 'YYYY-MM-DD' -> '(?P<YYYY>\d{4})-(?P<MM>\d{2})-(?P<DD>\d{2})'
|
||||
fmt_pattern = fmt
|
||||
tokens = []
|
||||
offset = 0
|
||||
for m in self._FORMAT_RE.finditer(fmt):
|
||||
token = m.group(0)
|
||||
try:
|
||||
input_re = self._input_re_map[token]
|
||||
except KeyError:
|
||||
raise ParserError('Unrecognized token \'{0}\''.format(token))
|
||||
input_pattern = '(?P<{0}>{1})'.format(token, input_re.pattern)
|
||||
tokens.append(token)
|
||||
# a pattern doesn't have the same length as the token
|
||||
# it replaces! We keep the difference in the offset variable.
|
||||
# This works because the string is scanned left-to-right and matches
|
||||
# are returned in the order found by finditer.
|
||||
fmt_pattern = fmt_pattern[:m.start() + offset] + input_pattern + fmt_pattern[m.end() + offset:]
|
||||
offset += len(input_pattern) - (m.end() - m.start())
|
||||
match = re.search(fmt_pattern, string, flags=re.IGNORECASE)
|
||||
if match is None:
|
||||
raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(fmt_pattern, string))
|
||||
parts = {}
|
||||
for token in tokens:
|
||||
if token == 'Do':
|
||||
value = match.group('value')
|
||||
else:
|
||||
value = match.group(token)
|
||||
self._parse_token(token, value, parts)
|
||||
return self._build_datetime(parts)
|
||||
|
||||
def _parse_token(self, token, value, parts):
|
||||
|
||||
if token == 'YYYY':
|
||||
parts['year'] = int(value)
|
||||
elif token == 'YY':
|
||||
value = int(value)
|
||||
parts['year'] = 1900 + value if value > 68 else 2000 + value
|
||||
|
||||
elif token in ['MMMM', 'MMM']:
|
||||
parts['month'] = self.locale.month_number(value.lower())
|
||||
|
||||
elif token in ['MM', 'M']:
|
||||
parts['month'] = int(value)
|
||||
|
||||
elif token in ['DD', 'D']:
|
||||
parts['day'] = int(value)
|
||||
|
||||
elif token in ['Do']:
|
||||
parts['day'] = int(value)
|
||||
|
||||
elif token.upper() in ['HH', 'H']:
|
||||
parts['hour'] = int(value)
|
||||
|
||||
elif token in ['mm', 'm']:
|
||||
parts['minute'] = int(value)
|
||||
|
||||
elif token in ['ss', 's']:
|
||||
parts['second'] = int(value)
|
||||
|
||||
elif token == 'SSSSSS':
|
||||
parts['microsecond'] = int(value)
|
||||
elif token == 'SSSSS':
|
||||
parts['microsecond'] = int(value) * 10
|
||||
elif token == 'SSSS':
|
||||
parts['microsecond'] = int(value) * 100
|
||||
elif token == 'SSS':
|
||||
parts['microsecond'] = int(value) * 1000
|
||||
elif token == 'SS':
|
||||
parts['microsecond'] = int(value) * 10000
|
||||
elif token == 'S':
|
||||
parts['microsecond'] = int(value) * 100000
|
||||
|
||||
elif token == 'X':
|
||||
parts['timestamp'] = int(value)
|
||||
|
||||
elif token in ['ZZZ', 'ZZ', 'Z']:
|
||||
parts['tzinfo'] = TzinfoParser.parse(value)
|
||||
|
||||
elif token in ['a', 'A']:
|
||||
if value in (
|
||||
self.locale.meridians['am'],
|
||||
self.locale.meridians['AM']
|
||||
):
|
||||
parts['am_pm'] = 'am'
|
||||
elif value in (
|
||||
self.locale.meridians['pm'],
|
||||
self.locale.meridians['PM']
|
||||
):
|
||||
parts['am_pm'] = 'pm'
|
||||
|
||||
@staticmethod
|
||||
def _build_datetime(parts):
|
||||
|
||||
timestamp = parts.get('timestamp')
|
||||
|
||||
if timestamp:
|
||||
tz_utc = tz.tzutc()
|
||||
return datetime.fromtimestamp(timestamp, tz=tz_utc)
|
||||
|
||||
am_pm = parts.get('am_pm')
|
||||
hour = parts.get('hour', 0)
|
||||
|
||||
if am_pm == 'pm' and hour < 12:
|
||||
hour += 12
|
||||
elif am_pm == 'am' and hour == 12:
|
||||
hour = 0
|
||||
|
||||
return datetime(year=parts.get('year', 1), month=parts.get('month', 1),
|
||||
day=parts.get('day', 1), hour=hour, minute=parts.get('minute', 0),
|
||||
second=parts.get('second', 0), microsecond=parts.get('microsecond', 0),
|
||||
tzinfo=parts.get('tzinfo'))
|
||||
|
||||
def _parse_multiformat(self, string, formats):
|
||||
|
||||
_datetime = None
|
||||
|
||||
for fmt in formats:
|
||||
try:
|
||||
_datetime = self.parse(string, fmt)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
if _datetime is None:
|
||||
raise ParserError('Could not match input to any of {0} on \'{1}\''.format(formats, string))
|
||||
|
||||
return _datetime
|
||||
|
||||
@staticmethod
|
||||
def _map_lookup(input_map, key):
|
||||
|
||||
try:
|
||||
return input_map[key]
|
||||
except KeyError:
|
||||
raise ParserError('Could not match "{0}" to {1}'.format(key, input_map))
|
||||
|
||||
@staticmethod
|
||||
def _try_timestamp(string):
|
||||
|
||||
try:
|
||||
return float(string)
|
||||
except:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _choice_re(choices, flags=0):
|
||||
return re.compile('({0})'.format('|'.join(choices)), flags=flags)
|
||||
|
||||
|
||||
class TzinfoParser(object):
|
||||
|
||||
_TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, string):
|
||||
|
||||
tzinfo = None
|
||||
|
||||
if string == 'local':
|
||||
tzinfo = tz.tzlocal()
|
||||
|
||||
elif string in ['utc', 'UTC']:
|
||||
tzinfo = tz.tzutc()
|
||||
|
||||
else:
|
||||
|
||||
iso_match = cls._TZINFO_RE.match(string)
|
||||
|
||||
if iso_match:
|
||||
sign, hours, minutes = iso_match.groups()
|
||||
seconds = int(hours) * 3600 + int(minutes) * 60
|
||||
|
||||
if sign == '-':
|
||||
seconds *= -1
|
||||
|
||||
tzinfo = tz.tzoffset(None, seconds)
|
||||
|
||||
else:
|
||||
tzinfo = tz.gettz(string)
|
||||
|
||||
if tzinfo is None:
|
||||
raise ParserError('Could not parse timezone expression "{0}"', string)
|
||||
|
||||
return tzinfo
|
45
lib/arrow/util.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
|
||||
# python 2.6 / 2.7 definitions for total_seconds function.
|
||||
|
||||
def _total_seconds_27(td): # pragma: no cover
|
||||
return td.total_seconds()
|
||||
|
||||
def _total_seconds_26(td):
|
||||
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6
|
||||
|
||||
|
||||
# get version info and assign correct total_seconds function.
|
||||
|
||||
version = '{0}.{1}.{2}'.format(*sys.version_info[:3])
|
||||
|
||||
if version < '2.7': # pragma: no cover
|
||||
total_seconds = _total_seconds_26
|
||||
else: # pragma: no cover
|
||||
total_seconds = _total_seconds_27
|
||||
|
||||
def is_timestamp(value):
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
# python 2.7 / 3.0+ definitions for isstr function.
|
||||
|
||||
try: # pragma: no cover
|
||||
basestring
|
||||
|
||||
def isstr(s):
|
||||
return isinstance(s, basestring)
|
||||
|
||||
except NameError: #pragma: no cover
|
||||
|
||||
def isstr(s):
|
||||
return isinstance(s, str)
|
||||
|
||||
|
||||
__all__ = ['total_seconds', 'is_timestamp', 'isstr']
|
@@ -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
|
||||
|
2
lib/dateutil/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__version__ = "2.4.2"
|
89
lib/dateutil/easter.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a generic easter computing method for any given year, using
|
||||
Western, Orthodox or Julian algorithms.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||
|
||||
EASTER_JULIAN = 1
|
||||
EASTER_ORTHODOX = 2
|
||||
EASTER_WESTERN = 3
|
||||
|
||||
|
||||
def easter(year, method=EASTER_WESTERN):
|
||||
"""
|
||||
This method was ported from the work done by GM Arts,
|
||||
on top of the algorithm by Claus Tondering, which was
|
||||
based in part on the algorithm of Ouding (1940), as
|
||||
quoted in "Explanatory Supplement to the Astronomical
|
||||
Almanac", P. Kenneth Seidelmann, editor.
|
||||
|
||||
This algorithm implements three different easter
|
||||
calculation methods:
|
||||
|
||||
1 - Original calculation in Julian calendar, valid in
|
||||
dates after 326 AD
|
||||
2 - Original method, with date converted to Gregorian
|
||||
calendar, valid in years 1583 to 4099
|
||||
3 - Revised method, in Gregorian calendar, valid in
|
||||
years 1583 to 4099 as well
|
||||
|
||||
These methods are represented by the constants:
|
||||
|
||||
EASTER_JULIAN = 1
|
||||
EASTER_ORTHODOX = 2
|
||||
EASTER_WESTERN = 3
|
||||
|
||||
The default method is method 3.
|
||||
|
||||
More about the algorithm may be found at:
|
||||
|
||||
http://users.chariot.net.au/~gmarts/eastalg.htm
|
||||
|
||||
and
|
||||
|
||||
http://www.tondering.dk/claus/calendar.html
|
||||
|
||||
"""
|
||||
|
||||
if not (1 <= method <= 3):
|
||||
raise ValueError("invalid method")
|
||||
|
||||
# g - Golden year - 1
|
||||
# c - Century
|
||||
# h - (23 - Epact) mod 30
|
||||
# i - Number of days from March 21 to Paschal Full Moon
|
||||
# j - Weekday for PFM (0=Sunday, etc)
|
||||
# p - Number of days from March 21 to Sunday on or before PFM
|
||||
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||
# e - Extra days to add for method 2 (converting Julian
|
||||
# date to Gregorian date)
|
||||
|
||||
y = year
|
||||
g = y % 19
|
||||
e = 0
|
||||
if method < 3:
|
||||
# Old method
|
||||
i = (19*g + 15) % 30
|
||||
j = (y + y//4 + i) % 7
|
||||
if method == 2:
|
||||
# Extra dates to convert Julian to Gregorian date
|
||||
e = 10
|
||||
if y > 1600:
|
||||
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||
else:
|
||||
# New method
|
||||
c = y//100
|
||||
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||
|
||||
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||
p = i - j + e
|
||||
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||
m = 3 + (p + 26)//30
|
||||
return datetime.date(int(y), int(m), int(d))
|
1205
lib/dateutil/parser.py
Normal file
450
lib/dateutil/relativedelta.py
Normal file
@@ -0,0 +1,450 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
from six import integer_types
|
||||
|
||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
|
||||
|
||||
|
||||
class relativedelta(object):
|
||||
"""
|
||||
The relativedelta type is based on the specification of the excellent
|
||||
work done by M.-A. Lemburg in his
|
||||
`mx.DateTime <http://www.egenix.com/files/python/mxDateTime.html>`_ extension.
|
||||
However, notice that this type does *NOT* implement the same algorithm as
|
||||
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||
|
||||
There are two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes::
|
||||
|
||||
relativedelta(datetime1, datetime2)
|
||||
|
||||
The second one is passing it any number of the following keyword arguments::
|
||||
|
||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||
|
||||
year, month, day, hour, minute, second, microsecond:
|
||||
Absolute information (argument is singular); adding or subtracting a
|
||||
relativedelta with absolute information does not perform an aritmetic
|
||||
operation, but rather REPLACES the corresponding value in the
|
||||
original datetime with the value(s) in relativedelta.
|
||||
|
||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||
Relative information, may be negative (argument is plural); adding
|
||||
or subtracting a relativedelta with relative information performs
|
||||
the corresponding aritmetic operation on the original datetime value
|
||||
with the information in the relativedelta.
|
||||
|
||||
weekday:
|
||||
One of the weekday instances (MO, TU, etc). These instances may
|
||||
receive a parameter N, specifying the Nth weekday, which could
|
||||
be positive or negative (like MO(+1) or MO(-2). Not specifying
|
||||
it is the same as specifying +1. You can also use an integer,
|
||||
where 0=MO.
|
||||
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
|
||||
Here is the behavior of operations with relativedelta:
|
||||
|
||||
1. Calculate the absolute year, using the 'year' argument, or the
|
||||
original datetime year, if the argument is not present.
|
||||
|
||||
2. Add the relative 'years' argument to the absolute year.
|
||||
|
||||
3. Do steps 1 and 2 for month/months.
|
||||
|
||||
4. Calculate the absolute day, using the 'day' argument, or the
|
||||
original datetime day, if the argument is not present. Then,
|
||||
subtract from the day until it fits in the year and month
|
||||
found after their operations.
|
||||
|
||||
5. Add the relative 'days' argument to the absolute day. Notice
|
||||
that the 'weeks' argument is multiplied by 7 and added to
|
||||
'days'.
|
||||
|
||||
6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
|
||||
microsecond/microseconds.
|
||||
|
||||
7. If the 'weekday' argument is present, calculate the weekday,
|
||||
with the given (wday, nth) tuple. wday is the index of the
|
||||
weekday (0-6, 0=Mon), and nth is the number of weeks to add
|
||||
forward or backward, depending on its signal. Notice that if
|
||||
the calculated date is already Monday, for example, using
|
||||
(0, 1) or (0, -1) won't change the day.
|
||||
"""
|
||||
|
||||
def __init__(self, dt1=None, dt2=None,
|
||||
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||
year=None, month=None, day=None, weekday=None,
|
||||
yearday=None, nlyearday=None,
|
||||
hour=None, minute=None, second=None, microsecond=None):
|
||||
if dt1 and dt2:
|
||||
# datetime is a subclass of date. So both must be date
|
||||
if not (isinstance(dt1, datetime.date) and
|
||||
isinstance(dt2, datetime.date)):
|
||||
raise TypeError("relativedelta only diffs datetime/date")
|
||||
# We allow two dates, or two datetimes, so we coerce them to be
|
||||
# of the same type
|
||||
if (isinstance(dt1, datetime.datetime) !=
|
||||
isinstance(dt2, datetime.datetime)):
|
||||
if not isinstance(dt1, datetime.datetime):
|
||||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||
elif not isinstance(dt2, datetime.datetime):
|
||||
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||
self.years = 0
|
||||
self.months = 0
|
||||
self.days = 0
|
||||
self.leapdays = 0
|
||||
self.hours = 0
|
||||
self.minutes = 0
|
||||
self.seconds = 0
|
||||
self.microseconds = 0
|
||||
self.year = None
|
||||
self.month = None
|
||||
self.day = None
|
||||
self.weekday = None
|
||||
self.hour = None
|
||||
self.minute = None
|
||||
self.second = None
|
||||
self.microsecond = None
|
||||
self._has_time = 0
|
||||
|
||||
months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month)
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
if dt1 < dt2:
|
||||
while dt1 > dtm:
|
||||
months += 1
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
else:
|
||||
while dt1 < dtm:
|
||||
months -= 1
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
delta = dt1 - dtm
|
||||
self.seconds = delta.seconds+delta.days*86400
|
||||
self.microseconds = delta.microseconds
|
||||
else:
|
||||
self.years = years
|
||||
self.months = months
|
||||
self.days = days+weeks*7
|
||||
self.leapdays = leapdays
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
self.seconds = seconds
|
||||
self.microseconds = microseconds
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
self.microsecond = microsecond
|
||||
|
||||
if isinstance(weekday, integer_types):
|
||||
self.weekday = weekdays[weekday]
|
||||
else:
|
||||
self.weekday = weekday
|
||||
|
||||
yday = 0
|
||||
if nlyearday:
|
||||
yday = nlyearday
|
||||
elif yearday:
|
||||
yday = yearday
|
||||
if yearday > 59:
|
||||
self.leapdays = -1
|
||||
if yday:
|
||||
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||
243, 273, 304, 334, 366]
|
||||
for idx, ydays in enumerate(ydayidx):
|
||||
if yday <= ydays:
|
||||
self.month = idx+1
|
||||
if idx == 0:
|
||||
self.day = yday
|
||||
else:
|
||||
self.day = yday-ydayidx[idx-1]
|
||||
break
|
||||
else:
|
||||
raise ValueError("invalid year day (%d)" % yday)
|
||||
|
||||
self._fix()
|
||||
|
||||
def _fix(self):
|
||||
if abs(self.microseconds) > 999999:
|
||||
s = self.microseconds//abs(self.microseconds)
|
||||
div, mod = divmod(self.microseconds*s, 1000000)
|
||||
self.microseconds = mod*s
|
||||
self.seconds += div*s
|
||||
if abs(self.seconds) > 59:
|
||||
s = self.seconds//abs(self.seconds)
|
||||
div, mod = divmod(self.seconds*s, 60)
|
||||
self.seconds = mod*s
|
||||
self.minutes += div*s
|
||||
if abs(self.minutes) > 59:
|
||||
s = self.minutes//abs(self.minutes)
|
||||
div, mod = divmod(self.minutes*s, 60)
|
||||
self.minutes = mod*s
|
||||
self.hours += div*s
|
||||
if abs(self.hours) > 23:
|
||||
s = self.hours//abs(self.hours)
|
||||
div, mod = divmod(self.hours*s, 24)
|
||||
self.hours = mod*s
|
||||
self.days += div*s
|
||||
if abs(self.months) > 11:
|
||||
s = self.months//abs(self.months)
|
||||
div, mod = divmod(self.months*s, 12)
|
||||
self.months = mod*s
|
||||
self.years += div*s
|
||||
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||
or self.hour is not None or self.minute is not None or
|
||||
self.second is not None or self.microsecond is not None):
|
||||
self._has_time = 1
|
||||
else:
|
||||
self._has_time = 0
|
||||
|
||||
def _set_months(self, months):
|
||||
self.months = months
|
||||
if abs(self.months) > 11:
|
||||
s = self.months//abs(self.months)
|
||||
div, mod = divmod(self.months*s, 12)
|
||||
self.months = mod*s
|
||||
self.years = div*s
|
||||
else:
|
||||
self.years = 0
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, relativedelta):
|
||||
return relativedelta(years=other.years+self.years,
|
||||
months=other.months+self.months,
|
||||
days=other.days+self.days,
|
||||
hours=other.hours+self.hours,
|
||||
minutes=other.minutes+self.minutes,
|
||||
seconds=other.seconds+self.seconds,
|
||||
microseconds=(other.microseconds +
|
||||
self.microseconds),
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=(other.microsecond or
|
||||
self.microsecond))
|
||||
if not isinstance(other, datetime.date):
|
||||
raise TypeError("unsupported type for add operation")
|
||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||
other = datetime.datetime.fromordinal(other.toordinal())
|
||||
year = (self.year or other.year)+self.years
|
||||
month = self.month or other.month
|
||||
if self.months:
|
||||
assert 1 <= abs(self.months) <= 12
|
||||
month += self.months
|
||||
if month > 12:
|
||||
year += 1
|
||||
month -= 12
|
||||
elif month < 1:
|
||||
year -= 1
|
||||
month += 12
|
||||
day = min(calendar.monthrange(year, month)[1],
|
||||
self.day or other.day)
|
||||
repl = {"year": year, "month": month, "day": day}
|
||||
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
repl[attr] = value
|
||||
days = self.days
|
||||
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||
days += self.leapdays
|
||||
ret = (other.replace(**repl)
|
||||
+ datetime.timedelta(days=days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds,
|
||||
microseconds=self.microseconds))
|
||||
if self.weekday:
|
||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||
jumpdays = (abs(nth)-1)*7
|
||||
if nth > 0:
|
||||
jumpdays += (7-ret.weekday()+weekday) % 7
|
||||
else:
|
||||
jumpdays += (ret.weekday()-weekday) % 7
|
||||
jumpdays *= -1
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__add__(other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__neg__().__radd__(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
raise TypeError("unsupported type for sub operation")
|
||||
return relativedelta(years=self.years-other.years,
|
||||
months=self.months-other.months,
|
||||
days=self.days-other.days,
|
||||
hours=self.hours-other.hours,
|
||||
minutes=self.minutes-other.minutes,
|
||||
seconds=self.seconds-other.seconds,
|
||||
microseconds=self.microseconds-other.microseconds,
|
||||
leapdays=self.leapdays or other.leapdays,
|
||||
year=self.year or other.year,
|
||||
month=self.month or other.month,
|
||||
day=self.day or other.day,
|
||||
weekday=self.weekday or other.weekday,
|
||||
hour=self.hour or other.hour,
|
||||
minute=self.minute or other.minute,
|
||||
second=self.second or other.second,
|
||||
microsecond=self.microsecond or other.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return relativedelta(years=-self.years,
|
||||
months=-self.months,
|
||||
days=-self.days,
|
||||
hours=-self.hours,
|
||||
minutes=-self.minutes,
|
||||
seconds=-self.seconds,
|
||||
microseconds=-self.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __bool__(self):
|
||||
return not (not self.years and
|
||||
not self.months and
|
||||
not self.days and
|
||||
not self.hours and
|
||||
not self.minutes and
|
||||
not self.seconds and
|
||||
not self.microseconds and
|
||||
not self.leapdays and
|
||||
self.year is None and
|
||||
self.month is None and
|
||||
self.day is None and
|
||||
self.weekday is None and
|
||||
self.hour is None and
|
||||
self.minute is None and
|
||||
self.second is None and
|
||||
self.microsecond is None)
|
||||
# Compatibility with Python 2.x
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __mul__(self, other):
|
||||
f = float(other)
|
||||
return relativedelta(years=int(self.years*f),
|
||||
months=int(self.months*f),
|
||||
days=int(self.days*f),
|
||||
hours=int(self.hours*f),
|
||||
minutes=int(self.minutes*f),
|
||||
seconds=int(self.seconds*f),
|
||||
microseconds=int(self.microseconds*f),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return False
|
||||
if self.weekday or other.weekday:
|
||||
if not self.weekday or not other.weekday:
|
||||
return False
|
||||
if self.weekday.weekday != other.weekday.weekday:
|
||||
return False
|
||||
n1, n2 = self.weekday.n, other.weekday.n
|
||||
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||
return False
|
||||
return (self.years == other.years and
|
||||
self.months == other.months and
|
||||
self.days == other.days and
|
||||
self.hours == other.hours and
|
||||
self.minutes == other.minutes and
|
||||
self.seconds == other.seconds and
|
||||
self.leapdays == other.leapdays and
|
||||
self.year == other.year and
|
||||
self.month == other.month and
|
||||
self.day == other.day and
|
||||
self.hour == other.hour and
|
||||
self.minute == other.minute and
|
||||
self.second == other.second and
|
||||
self.microsecond == other.microsecond)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __div__(self, other):
|
||||
return self.__mul__(1/float(other))
|
||||
|
||||
__truediv__ = __div__
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in ["years", "months", "days", "leapdays",
|
||||
"hours", "minutes", "seconds", "microseconds"]:
|
||||
value = getattr(self, attr)
|
||||
if value:
|
||||
l.append("%s=%+d" % (attr, value))
|
||||
for attr in ["year", "month", "day", "weekday",
|
||||
"hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, repr(value)))
|
||||
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||||
|
||||
# vim:ts=4:sw=4:et
|
1375
lib/dateutil/rrule.py
Normal file
986
lib/dateutil/tz.py
Normal file
@@ -0,0 +1,986 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers timezone implementations subclassing the abstract
|
||||
:py:`datetime.tzinfo` type. There are classes to handle tzfile format files
|
||||
(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ
|
||||
environment string (in all known formats), given ranges (with help from
|
||||
relative deltas), local machine timezone, fixed offset timezone, and UTC
|
||||
timezone.
|
||||
"""
|
||||
import datetime
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
from six import string_types, PY3
|
||||
|
||||
try:
|
||||
from dateutil.tzwin import tzwin, tzwinlocal
|
||||
except ImportError:
|
||||
tzwin = tzwinlocal = None
|
||||
|
||||
relativedelta = None
|
||||
parser = None
|
||||
rrule = None
|
||||
|
||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"]
|
||||
|
||||
|
||||
def tzname_in_python2(myfunc):
|
||||
"""Change unicode output into bytestrings in Python 2
|
||||
|
||||
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||
to unicode strings
|
||||
"""
|
||||
def inner_func(*args, **kwargs):
|
||||
if PY3:
|
||||
return myfunc(*args, **kwargs)
|
||||
else:
|
||||
return myfunc(*args, **kwargs).encode()
|
||||
return inner_func
|
||||
|
||||
ZERO = datetime.timedelta(0)
|
||||
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
|
||||
|
||||
|
||||
class tzutc(datetime.tzinfo):
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return ZERO
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, tzutc) or
|
||||
(isinstance(other, tzoffset) and other._offset == ZERO))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s()" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzoffset(datetime.tzinfo):
|
||||
|
||||
def __init__(self, name, offset):
|
||||
self._name = name
|
||||
self._offset = datetime.timedelta(seconds=offset)
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._offset
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return self._name
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, tzoffset) and
|
||||
self._offset == other._offset)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s, %s)" % (self.__class__.__name__,
|
||||
repr(self._name),
|
||||
self._offset.days*86400+self._offset.seconds)
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzlocal(datetime.tzinfo):
|
||||
|
||||
_std_offset = datetime.timedelta(seconds=-time.timezone)
|
||||
if time.daylight:
|
||||
_dst_offset = datetime.timedelta(seconds=-time.altzone)
|
||||
else:
|
||||
_dst_offset = _std_offset
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset-self._std_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return time.tzname[self._isdst(dt)]
|
||||
|
||||
def _isdst(self, dt):
|
||||
# We can't use mktime here. It is unstable when deciding if
|
||||
# the hour near to a change is DST or not.
|
||||
#
|
||||
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
|
||||
# dt.minute, dt.second, dt.weekday(), 0, -1))
|
||||
# return time.localtime(timestamp).tm_isdst
|
||||
#
|
||||
# The code above yields the following result:
|
||||
#
|
||||
# >>> import tz, datetime
|
||||
# >>> t = tz.tzlocal()
|
||||
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||||
# 'BRDT'
|
||||
# >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
|
||||
# 'BRST'
|
||||
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||||
# 'BRST'
|
||||
# >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
|
||||
# 'BRDT'
|
||||
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||||
# 'BRDT'
|
||||
#
|
||||
# Here is a more stable implementation:
|
||||
#
|
||||
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ dt.hour * 3600
|
||||
+ dt.minute * 60
|
||||
+ dt.second)
|
||||
return time.localtime(timestamp+time.timezone).tm_isdst
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, tzlocal):
|
||||
return False
|
||||
return (self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset)
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s()" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class _ttinfo(object):
|
||||
__slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
|
||||
|
||||
def __init__(self):
|
||||
for attr in self.__slots__:
|
||||
setattr(self, attr, None)
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in self.__slots__:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, repr(value)))
|
||||
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, _ttinfo):
|
||||
return False
|
||||
return (self.offset == other.offset and
|
||||
self.delta == other.delta and
|
||||
self.isdst == other.isdst and
|
||||
self.abbr == other.abbr and
|
||||
self.isstd == other.isstd and
|
||||
self.isgmt == other.isgmt)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __getstate__(self):
|
||||
state = {}
|
||||
for name in self.__slots__:
|
||||
state[name] = getattr(self, name, None)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
for name in self.__slots__:
|
||||
if name in state:
|
||||
setattr(self, name, state[name])
|
||||
|
||||
|
||||
class tzfile(datetime.tzinfo):
|
||||
|
||||
# http://www.twinsun.com/tz/tz-link.htm
|
||||
# ftp://ftp.iana.org/tz/tz*.tar.gz
|
||||
|
||||
def __init__(self, fileobj, filename=None):
|
||||
file_opened_here = False
|
||||
if isinstance(fileobj, string_types):
|
||||
self._filename = fileobj
|
||||
fileobj = open(fileobj, 'rb')
|
||||
file_opened_here = True
|
||||
elif filename is not None:
|
||||
self._filename = filename
|
||||
elif hasattr(fileobj, "name"):
|
||||
self._filename = fileobj.name
|
||||
else:
|
||||
self._filename = repr(fileobj)
|
||||
|
||||
# From tzfile(5):
|
||||
#
|
||||
# The time zone information files used by tzset(3)
|
||||
# begin with the magic characters "TZif" to identify
|
||||
# them as time zone information files, followed by
|
||||
# sixteen bytes reserved for future use, followed by
|
||||
# six four-byte values of type long, written in a
|
||||
# ``standard'' byte order (the high-order byte
|
||||
# of the value is written first).
|
||||
try:
|
||||
if fileobj.read(4).decode() != "TZif":
|
||||
raise ValueError("magic not found")
|
||||
|
||||
fileobj.read(16)
|
||||
|
||||
(
|
||||
# The number of UTC/local indicators stored in the file.
|
||||
ttisgmtcnt,
|
||||
|
||||
# The number of standard/wall indicators stored in the file.
|
||||
ttisstdcnt,
|
||||
|
||||
# The number of leap seconds for which data is
|
||||
# stored in the file.
|
||||
leapcnt,
|
||||
|
||||
# The number of "transition times" for which data
|
||||
# is stored in the file.
|
||||
timecnt,
|
||||
|
||||
# The number of "local time types" for which data
|
||||
# is stored in the file (must not be zero).
|
||||
typecnt,
|
||||
|
||||
# The number of characters of "time zone
|
||||
# abbreviation strings" stored in the file.
|
||||
charcnt,
|
||||
|
||||
) = struct.unpack(">6l", fileobj.read(24))
|
||||
|
||||
# The above header is followed by tzh_timecnt four-byte
|
||||
# values of type long, sorted in ascending order.
|
||||
# These values are written in ``standard'' byte order.
|
||||
# Each is used as a transition time (as returned by
|
||||
# time(2)) at which the rules for computing local time
|
||||
# change.
|
||||
|
||||
if timecnt:
|
||||
self._trans_list = struct.unpack(">%dl" % timecnt,
|
||||
fileobj.read(timecnt*4))
|
||||
else:
|
||||
self._trans_list = []
|
||||
|
||||
# Next come tzh_timecnt one-byte values of type unsigned
|
||||
# char; each one tells which of the different types of
|
||||
# ``local time'' types described in the file is associated
|
||||
# with the same-indexed transition time. These values
|
||||
# serve as indices into an array of ttinfo structures that
|
||||
# appears next in the file.
|
||||
|
||||
if timecnt:
|
||||
self._trans_idx = struct.unpack(">%dB" % timecnt,
|
||||
fileobj.read(timecnt))
|
||||
else:
|
||||
self._trans_idx = []
|
||||
|
||||
# Each ttinfo structure is written as a four-byte value
|
||||
# for tt_gmtoff of type long, in a standard byte
|
||||
# order, followed by a one-byte value for tt_isdst
|
||||
# and a one-byte value for tt_abbrind. In each
|
||||
# structure, tt_gmtoff gives the number of
|
||||
# seconds to be added to UTC, tt_isdst tells whether
|
||||
# tm_isdst should be set by localtime(3), and
|
||||
# tt_abbrind serves as an index into the array of
|
||||
# time zone abbreviation characters that follow the
|
||||
# ttinfo structure(s) in the file.
|
||||
|
||||
ttinfo = []
|
||||
|
||||
for i in range(typecnt):
|
||||
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
|
||||
|
||||
abbr = fileobj.read(charcnt).decode()
|
||||
|
||||
# Then there are tzh_leapcnt pairs of four-byte
|
||||
# values, written in standard byte order; the
|
||||
# first value of each pair gives the time (as
|
||||
# returned by time(2)) at which a leap second
|
||||
# occurs; the second gives the total number of
|
||||
# leap seconds to be applied after the given time.
|
||||
# The pairs of values are sorted in ascending order
|
||||
# by time.
|
||||
|
||||
# Not used, for now
|
||||
# if leapcnt:
|
||||
# leap = struct.unpack(">%dl" % (leapcnt*2),
|
||||
# fileobj.read(leapcnt*8))
|
||||
|
||||
# Then there are tzh_ttisstdcnt standard/wall
|
||||
# indicators, each stored as a one-byte value;
|
||||
# they tell whether the transition times associated
|
||||
# with local time types were specified as standard
|
||||
# time or wall clock time, and are used when
|
||||
# a time zone file is used in handling POSIX-style
|
||||
# time zone environment variables.
|
||||
|
||||
if ttisstdcnt:
|
||||
isstd = struct.unpack(">%db" % ttisstdcnt,
|
||||
fileobj.read(ttisstdcnt))
|
||||
|
||||
# Finally, there are tzh_ttisgmtcnt UTC/local
|
||||
# indicators, each stored as a one-byte value;
|
||||
# they tell whether the transition times associated
|
||||
# with local time types were specified as UTC or
|
||||
# local time, and are used when a time zone file
|
||||
# is used in handling POSIX-style time zone envi-
|
||||
# ronment variables.
|
||||
|
||||
if ttisgmtcnt:
|
||||
isgmt = struct.unpack(">%db" % ttisgmtcnt,
|
||||
fileobj.read(ttisgmtcnt))
|
||||
|
||||
# ** Everything has been read **
|
||||
finally:
|
||||
if file_opened_here:
|
||||
fileobj.close()
|
||||
|
||||
# Build ttinfo list
|
||||
self._ttinfo_list = []
|
||||
for i in range(typecnt):
|
||||
gmtoff, isdst, abbrind = ttinfo[i]
|
||||
# Round to full-minutes if that's not the case. Python's
|
||||
# datetime doesn't accept sub-minute timezones. Check
|
||||
# http://python.org/sf/1447945 for some information.
|
||||
gmtoff = (gmtoff+30)//60*60
|
||||
tti = _ttinfo()
|
||||
tti.offset = gmtoff
|
||||
tti.delta = datetime.timedelta(seconds=gmtoff)
|
||||
tti.isdst = isdst
|
||||
tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
|
||||
tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
|
||||
tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
|
||||
self._ttinfo_list.append(tti)
|
||||
|
||||
# Replace ttinfo indexes for ttinfo objects.
|
||||
trans_idx = []
|
||||
for idx in self._trans_idx:
|
||||
trans_idx.append(self._ttinfo_list[idx])
|
||||
self._trans_idx = tuple(trans_idx)
|
||||
|
||||
# Set standard, dst, and before ttinfos. before will be
|
||||
# used when a given time is before any transitions,
|
||||
# and will be set to the first non-dst ttinfo, or to
|
||||
# the first dst, if all of them are dst.
|
||||
self._ttinfo_std = None
|
||||
self._ttinfo_dst = None
|
||||
self._ttinfo_before = None
|
||||
if self._ttinfo_list:
|
||||
if not self._trans_list:
|
||||
self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
|
||||
else:
|
||||
for i in range(timecnt-1, -1, -1):
|
||||
tti = self._trans_idx[i]
|
||||
if not self._ttinfo_std and not tti.isdst:
|
||||
self._ttinfo_std = tti
|
||||
elif not self._ttinfo_dst and tti.isdst:
|
||||
self._ttinfo_dst = tti
|
||||
if self._ttinfo_std and self._ttinfo_dst:
|
||||
break
|
||||
else:
|
||||
if self._ttinfo_dst and not self._ttinfo_std:
|
||||
self._ttinfo_std = self._ttinfo_dst
|
||||
|
||||
for tti in self._ttinfo_list:
|
||||
if not tti.isdst:
|
||||
self._ttinfo_before = tti
|
||||
break
|
||||
else:
|
||||
self._ttinfo_before = self._ttinfo_list[0]
|
||||
|
||||
# Now fix transition times to become relative to wall time.
|
||||
#
|
||||
# I'm not sure about this. In my tests, the tz source file
|
||||
# is setup to wall time, and in the binary file isstd and
|
||||
# isgmt are off, so it should be in wall time. OTOH, it's
|
||||
# always in gmt time. Let me know if you have comments
|
||||
# about this.
|
||||
laststdoffset = 0
|
||||
self._trans_list = list(self._trans_list)
|
||||
for i in range(len(self._trans_list)):
|
||||
tti = self._trans_idx[i]
|
||||
if not tti.isdst:
|
||||
# This is std time.
|
||||
self._trans_list[i] += tti.offset
|
||||
laststdoffset = tti.offset
|
||||
else:
|
||||
# This is dst time. Convert to std.
|
||||
self._trans_list[i] += laststdoffset
|
||||
self._trans_list = tuple(self._trans_list)
|
||||
|
||||
def _find_ttinfo(self, dt, laststd=0):
|
||||
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ dt.hour * 3600
|
||||
+ dt.minute * 60
|
||||
+ dt.second)
|
||||
idx = 0
|
||||
for trans in self._trans_list:
|
||||
if timestamp < trans:
|
||||
break
|
||||
idx += 1
|
||||
else:
|
||||
return self._ttinfo_std
|
||||
if idx == 0:
|
||||
return self._ttinfo_before
|
||||
if laststd:
|
||||
while idx > 0:
|
||||
tti = self._trans_idx[idx-1]
|
||||
if not tti.isdst:
|
||||
return tti
|
||||
idx -= 1
|
||||
else:
|
||||
return self._ttinfo_std
|
||||
else:
|
||||
return self._trans_idx[idx-1]
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if not self._ttinfo_std:
|
||||
return ZERO
|
||||
return self._find_ttinfo(dt).delta
|
||||
|
||||
def dst(self, dt):
|
||||
if not self._ttinfo_dst:
|
||||
return ZERO
|
||||
tti = self._find_ttinfo(dt)
|
||||
if not tti.isdst:
|
||||
return ZERO
|
||||
|
||||
# The documentation says that utcoffset()-dst() must
|
||||
# be constant for every dt.
|
||||
return tti.delta-self._find_ttinfo(dt, laststd=1).delta
|
||||
|
||||
# An alternative for that would be:
|
||||
#
|
||||
# return self._ttinfo_dst.offset-self._ttinfo_std.offset
|
||||
#
|
||||
# However, this class stores historical changes in the
|
||||
# dst offset, so I belive that this wouldn't be the right
|
||||
# way to implement this.
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if not self._ttinfo_std:
|
||||
return None
|
||||
return self._find_ttinfo(dt).abbr
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, tzfile):
|
||||
return False
|
||||
return (self._trans_list == other._trans_list and
|
||||
self._trans_idx == other._trans_idx and
|
||||
self._ttinfo_list == other._ttinfo_list)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
|
||||
|
||||
def __reduce__(self):
|
||||
if not os.path.isfile(self._filename):
|
||||
raise ValueError("Unpickable %s class" % self.__class__.__name__)
|
||||
return (self.__class__, (self._filename,))
|
||||
|
||||
|
||||
class tzrange(datetime.tzinfo):
|
||||
def __init__(self, stdabbr, stdoffset=None,
|
||||
dstabbr=None, dstoffset=None,
|
||||
start=None, end=None):
|
||||
global relativedelta
|
||||
if not relativedelta:
|
||||
from dateutil import relativedelta
|
||||
self._std_abbr = stdabbr
|
||||
self._dst_abbr = dstabbr
|
||||
if stdoffset is not None:
|
||||
self._std_offset = datetime.timedelta(seconds=stdoffset)
|
||||
else:
|
||||
self._std_offset = ZERO
|
||||
if dstoffset is not None:
|
||||
self._dst_offset = datetime.timedelta(seconds=dstoffset)
|
||||
elif dstabbr and stdoffset is not None:
|
||||
self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
|
||||
else:
|
||||
self._dst_offset = ZERO
|
||||
if dstabbr and start is None:
|
||||
self._start_delta = relativedelta.relativedelta(
|
||||
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
|
||||
else:
|
||||
self._start_delta = start
|
||||
if dstabbr and end is None:
|
||||
self._end_delta = relativedelta.relativedelta(
|
||||
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
|
||||
else:
|
||||
self._end_delta = end
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset-self._std_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_abbr
|
||||
else:
|
||||
return self._std_abbr
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self._start_delta:
|
||||
return False
|
||||
year = datetime.datetime(dt.year, 1, 1)
|
||||
start = year+self._start_delta
|
||||
end = year+self._end_delta
|
||||
dt = dt.replace(tzinfo=None)
|
||||
if start < end:
|
||||
return dt >= start and dt < end
|
||||
else:
|
||||
return dt >= start or dt < end
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, tzrange):
|
||||
return False
|
||||
return (self._std_abbr == other._std_abbr and
|
||||
self._dst_abbr == other._dst_abbr and
|
||||
self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset and
|
||||
self._start_delta == other._start_delta and
|
||||
self._end_delta == other._end_delta)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(...)" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzstr(tzrange):
|
||||
|
||||
def __init__(self, s):
|
||||
global parser
|
||||
if not parser:
|
||||
from dateutil import parser
|
||||
self._s = s
|
||||
|
||||
res = parser._parsetz(s)
|
||||
if res is None:
|
||||
raise ValueError("unknown string format")
|
||||
|
||||
# Here we break the compatibility with the TZ variable handling.
|
||||
# GMT-3 actually *means* the timezone -3.
|
||||
if res.stdabbr in ("GMT", "UTC"):
|
||||
res.stdoffset *= -1
|
||||
|
||||
# We must initialize it first, since _delta() needs
|
||||
# _std_offset and _dst_offset set. Use False in start/end
|
||||
# to avoid building it two times.
|
||||
tzrange.__init__(self, res.stdabbr, res.stdoffset,
|
||||
res.dstabbr, res.dstoffset,
|
||||
start=False, end=False)
|
||||
|
||||
if not res.dstabbr:
|
||||
self._start_delta = None
|
||||
self._end_delta = None
|
||||
else:
|
||||
self._start_delta = self._delta(res.start)
|
||||
if self._start_delta:
|
||||
self._end_delta = self._delta(res.end, isend=1)
|
||||
|
||||
def _delta(self, x, isend=0):
|
||||
kwargs = {}
|
||||
if x.month is not None:
|
||||
kwargs["month"] = x.month
|
||||
if x.weekday is not None:
|
||||
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
|
||||
if x.week > 0:
|
||||
kwargs["day"] = 1
|
||||
else:
|
||||
kwargs["day"] = 31
|
||||
elif x.day:
|
||||
kwargs["day"] = x.day
|
||||
elif x.yday is not None:
|
||||
kwargs["yearday"] = x.yday
|
||||
elif x.jyday is not None:
|
||||
kwargs["nlyearday"] = x.jyday
|
||||
if not kwargs:
|
||||
# Default is to start on first sunday of april, and end
|
||||
# on last sunday of october.
|
||||
if not isend:
|
||||
kwargs["month"] = 4
|
||||
kwargs["day"] = 1
|
||||
kwargs["weekday"] = relativedelta.SU(+1)
|
||||
else:
|
||||
kwargs["month"] = 10
|
||||
kwargs["day"] = 31
|
||||
kwargs["weekday"] = relativedelta.SU(-1)
|
||||
if x.time is not None:
|
||||
kwargs["seconds"] = x.time
|
||||
else:
|
||||
# Default is 2AM.
|
||||
kwargs["seconds"] = 7200
|
||||
if isend:
|
||||
# Convert to standard time, to follow the documented way
|
||||
# of working with the extra hour. See the documentation
|
||||
# of the tzinfo class.
|
||||
delta = self._dst_offset-self._std_offset
|
||||
kwargs["seconds"] -= delta.seconds+delta.days*86400
|
||||
return relativedelta.relativedelta(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
|
||||
|
||||
|
||||
class _tzicalvtzcomp(object):
|
||||
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
|
||||
tzname=None, rrule=None):
|
||||
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
|
||||
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
|
||||
self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
|
||||
self.isdst = isdst
|
||||
self.tzname = tzname
|
||||
self.rrule = rrule
|
||||
|
||||
|
||||
class _tzicalvtz(datetime.tzinfo):
|
||||
def __init__(self, tzid, comps=[]):
|
||||
self._tzid = tzid
|
||||
self._comps = comps
|
||||
self._cachedate = []
|
||||
self._cachecomp = []
|
||||
|
||||
def _find_comp(self, dt):
|
||||
if len(self._comps) == 1:
|
||||
return self._comps[0]
|
||||
dt = dt.replace(tzinfo=None)
|
||||
try:
|
||||
return self._cachecomp[self._cachedate.index(dt)]
|
||||
except ValueError:
|
||||
pass
|
||||
lastcomp = None
|
||||
lastcompdt = None
|
||||
for comp in self._comps:
|
||||
if not comp.isdst:
|
||||
# Handle the extra hour in DST -> STD
|
||||
compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
|
||||
else:
|
||||
compdt = comp.rrule.before(dt, inc=True)
|
||||
if compdt and (not lastcompdt or lastcompdt < compdt):
|
||||
lastcompdt = compdt
|
||||
lastcomp = comp
|
||||
if not lastcomp:
|
||||
# RFC says nothing about what to do when a given
|
||||
# time is before the first onset date. We'll look for the
|
||||
# first standard component, or the first component, if
|
||||
# none is found.
|
||||
for comp in self._comps:
|
||||
if not comp.isdst:
|
||||
lastcomp = comp
|
||||
break
|
||||
else:
|
||||
lastcomp = comp[0]
|
||||
self._cachedate.insert(0, dt)
|
||||
self._cachecomp.insert(0, lastcomp)
|
||||
if len(self._cachedate) > 10:
|
||||
self._cachedate.pop()
|
||||
self._cachecomp.pop()
|
||||
return lastcomp
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._find_comp(dt).tzoffsetto
|
||||
|
||||
def dst(self, dt):
|
||||
comp = self._find_comp(dt)
|
||||
if comp.isdst:
|
||||
return comp.tzoffsetdiff
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return self._find_comp(dt).tzname
|
||||
|
||||
def __repr__(self):
|
||||
return "<tzicalvtz %s>" % repr(self._tzid)
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzical(object):
|
||||
def __init__(self, fileobj):
|
||||
global rrule
|
||||
if not rrule:
|
||||
from dateutil import rrule
|
||||
|
||||
if isinstance(fileobj, string_types):
|
||||
self._s = fileobj
|
||||
# ical should be encoded in UTF-8 with CRLF
|
||||
fileobj = open(fileobj, 'r')
|
||||
elif hasattr(fileobj, "name"):
|
||||
self._s = fileobj.name
|
||||
else:
|
||||
self._s = repr(fileobj)
|
||||
|
||||
self._vtz = {}
|
||||
|
||||
self._parse_rfc(fileobj.read())
|
||||
|
||||
def keys(self):
|
||||
return list(self._vtz.keys())
|
||||
|
||||
def get(self, tzid=None):
|
||||
if tzid is None:
|
||||
keys = list(self._vtz.keys())
|
||||
if len(keys) == 0:
|
||||
raise ValueError("no timezones defined")
|
||||
elif len(keys) > 1:
|
||||
raise ValueError("more than one timezone available")
|
||||
tzid = keys[0]
|
||||
return self._vtz.get(tzid)
|
||||
|
||||
def _parse_offset(self, s):
|
||||
s = s.strip()
|
||||
if not s:
|
||||
raise ValueError("empty offset")
|
||||
if s[0] in ('+', '-'):
|
||||
signal = (-1, +1)[s[0] == '+']
|
||||
s = s[1:]
|
||||
else:
|
||||
signal = +1
|
||||
if len(s) == 4:
|
||||
return (int(s[:2])*3600+int(s[2:])*60)*signal
|
||||
elif len(s) == 6:
|
||||
return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
|
||||
else:
|
||||
raise ValueError("invalid offset: "+s)
|
||||
|
||||
def _parse_rfc(self, s):
|
||||
lines = s.splitlines()
|
||||
if not lines:
|
||||
raise ValueError("empty string")
|
||||
|
||||
# Unfold
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].rstrip()
|
||||
if not line:
|
||||
del lines[i]
|
||||
elif i > 0 and line[0] == " ":
|
||||
lines[i-1] += line[1:]
|
||||
del lines[i]
|
||||
else:
|
||||
i += 1
|
||||
|
||||
tzid = None
|
||||
comps = []
|
||||
invtz = False
|
||||
comptype = None
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
name, value = line.split(':', 1)
|
||||
parms = name.split(';')
|
||||
if not parms:
|
||||
raise ValueError("empty property name")
|
||||
name = parms[0].upper()
|
||||
parms = parms[1:]
|
||||
if invtz:
|
||||
if name == "BEGIN":
|
||||
if value in ("STANDARD", "DAYLIGHT"):
|
||||
# Process component
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unknown component: "+value)
|
||||
comptype = value
|
||||
founddtstart = False
|
||||
tzoffsetfrom = None
|
||||
tzoffsetto = None
|
||||
rrulelines = []
|
||||
tzname = None
|
||||
elif name == "END":
|
||||
if value == "VTIMEZONE":
|
||||
if comptype:
|
||||
raise ValueError("component not closed: "+comptype)
|
||||
if not tzid:
|
||||
raise ValueError("mandatory TZID not found")
|
||||
if not comps:
|
||||
raise ValueError(
|
||||
"at least one component is needed")
|
||||
# Process vtimezone
|
||||
self._vtz[tzid] = _tzicalvtz(tzid, comps)
|
||||
invtz = False
|
||||
elif value == comptype:
|
||||
if not founddtstart:
|
||||
raise ValueError("mandatory DTSTART not found")
|
||||
if tzoffsetfrom is None:
|
||||
raise ValueError(
|
||||
"mandatory TZOFFSETFROM not found")
|
||||
if tzoffsetto is None:
|
||||
raise ValueError(
|
||||
"mandatory TZOFFSETFROM not found")
|
||||
# Process component
|
||||
rr = None
|
||||
if rrulelines:
|
||||
rr = rrule.rrulestr("\n".join(rrulelines),
|
||||
compatible=True,
|
||||
ignoretz=True,
|
||||
cache=True)
|
||||
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
|
||||
(comptype == "DAYLIGHT"),
|
||||
tzname, rr)
|
||||
comps.append(comp)
|
||||
comptype = None
|
||||
else:
|
||||
raise ValueError("invalid component end: "+value)
|
||||
elif comptype:
|
||||
if name == "DTSTART":
|
||||
rrulelines.append(line)
|
||||
founddtstart = True
|
||||
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
|
||||
rrulelines.append(line)
|
||||
elif name == "TZOFFSETFROM":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported %s parm: %s " % (name, parms[0]))
|
||||
tzoffsetfrom = self._parse_offset(value)
|
||||
elif name == "TZOFFSETTO":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported TZOFFSETTO parm: "+parms[0])
|
||||
tzoffsetto = self._parse_offset(value)
|
||||
elif name == "TZNAME":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported TZNAME parm: "+parms[0])
|
||||
tzname = value
|
||||
elif name == "COMMENT":
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unsupported property: "+name)
|
||||
else:
|
||||
if name == "TZID":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported TZID parm: "+parms[0])
|
||||
tzid = value
|
||||
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unsupported property: "+name)
|
||||
elif name == "BEGIN" and value == "VTIMEZONE":
|
||||
tzid = None
|
||||
comps = []
|
||||
invtz = True
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
|
||||
|
||||
if sys.platform != "win32":
|
||||
TZFILES = ["/etc/localtime", "localtime"]
|
||||
TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
|
||||
else:
|
||||
TZFILES = []
|
||||
TZPATHS = []
|
||||
|
||||
|
||||
def gettz(name=None):
|
||||
tz = None
|
||||
if not name:
|
||||
try:
|
||||
name = os.environ["TZ"]
|
||||
except KeyError:
|
||||
pass
|
||||
if name is None or name == ":":
|
||||
for filepath in TZFILES:
|
||||
if not os.path.isabs(filepath):
|
||||
filename = filepath
|
||||
for path in TZPATHS:
|
||||
filepath = os.path.join(path, filename)
|
||||
if os.path.isfile(filepath):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if os.path.isfile(filepath):
|
||||
try:
|
||||
tz = tzfile(filepath)
|
||||
break
|
||||
except (IOError, OSError, ValueError):
|
||||
pass
|
||||
else:
|
||||
tz = tzlocal()
|
||||
else:
|
||||
if name.startswith(":"):
|
||||
name = name[:-1]
|
||||
if os.path.isabs(name):
|
||||
if os.path.isfile(name):
|
||||
tz = tzfile(name)
|
||||
else:
|
||||
tz = None
|
||||
else:
|
||||
for path in TZPATHS:
|
||||
filepath = os.path.join(path, name)
|
||||
if not os.path.isfile(filepath):
|
||||
filepath = filepath.replace(' ', '_')
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
try:
|
||||
tz = tzfile(filepath)
|
||||
break
|
||||
except (IOError, OSError, ValueError):
|
||||
pass
|
||||
else:
|
||||
tz = None
|
||||
if tzwin is not None:
|
||||
try:
|
||||
tz = tzwin(name)
|
||||
except WindowsError:
|
||||
tz = None
|
||||
if not tz:
|
||||
from dateutil.zoneinfo import gettz
|
||||
tz = gettz(name)
|
||||
if not tz:
|
||||
for c in name:
|
||||
# name must have at least one offset to be a tzstr
|
||||
if c in "0123456789":
|
||||
try:
|
||||
tz = tzstr(name)
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
else:
|
||||
if name in ("GMT", "UTC"):
|
||||
tz = tzutc()
|
||||
elif name in time.tzname:
|
||||
tz = tzlocal()
|
||||
return tz
|
||||
|
||||
# vim:ts=4:sw=4:et
|
184
lib/dateutil/tzwin.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# This code was originally contributed by Jeffrey Harris.
|
||||
import datetime
|
||||
import struct
|
||||
|
||||
from six.moves import winreg
|
||||
|
||||
__all__ = ["tzwin", "tzwinlocal"]
|
||||
|
||||
ONEWEEK = datetime.timedelta(7)
|
||||
|
||||
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||
|
||||
|
||||
def _settzkeyname():
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
try:
|
||||
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||
TZKEYNAME = TZKEYNAMENT
|
||||
except WindowsError:
|
||||
TZKEYNAME = TZKEYNAME9X
|
||||
handle.Close()
|
||||
return TZKEYNAME
|
||||
|
||||
TZKEYNAME = _settzkeyname()
|
||||
|
||||
|
||||
class tzwinbase(datetime.tzinfo):
|
||||
"""tzinfo class based on win32's timezones available in the registry."""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return datetime.timedelta(minutes=self._dstoffset)
|
||||
else:
|
||||
return datetime.timedelta(minutes=self._stdoffset)
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
minutes = self._dstoffset - self._stdoffset
|
||||
return datetime.timedelta(minutes=minutes)
|
||||
else:
|
||||
return datetime.timedelta(0)
|
||||
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dstname
|
||||
else:
|
||||
return self._stdname
|
||||
|
||||
def list():
|
||||
"""Return a list of all time zones known to the system."""
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
tzkey = winreg.OpenKey(handle, TZKEYNAME)
|
||||
result = [winreg.EnumKey(tzkey, i)
|
||||
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||
tzkey.Close()
|
||||
handle.Close()
|
||||
return result
|
||||
list = staticmethod(list)
|
||||
|
||||
def display(self):
|
||||
return self._display
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self._dstmonth:
|
||||
# dstmonth == 0 signals the zone has no daylight saving time
|
||||
return False
|
||||
dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek,
|
||||
self._dsthour, self._dstminute,
|
||||
self._dstweeknumber)
|
||||
dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek,
|
||||
self._stdhour, self._stdminute,
|
||||
self._stdweeknumber)
|
||||
if dston < dstoff:
|
||||
return dston <= dt.replace(tzinfo=None) < dstoff
|
||||
else:
|
||||
return not dstoff <= dt.replace(tzinfo=None) < dston
|
||||
|
||||
|
||||
class tzwin(tzwinbase):
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
# multiple contexts only possible in 2.7 and 3.1, we still support 2.6
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle,
|
||||
"%s\%s" % (TZKEYNAME, name)) as tzkey:
|
||||
keydict = valuestodict(tzkey)
|
||||
|
||||
self._stdname = keydict["Std"].encode("iso-8859-1")
|
||||
self._dstname = keydict["Dlt"].encode("iso-8859-1")
|
||||
|
||||
self._display = keydict["Display"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||
self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||
self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1
|
||||
|
||||
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[4:9]
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[12:17]
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwin(%s)" % repr(self._name)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self._name,))
|
||||
|
||||
|
||||
class tzwinlocal(tzwinbase):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
|
||||
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||
keydict = valuestodict(tzlocalkey)
|
||||
|
||||
self._stdname = keydict["StandardName"].encode("iso-8859-1")
|
||||
self._dstname = keydict["DaylightName"].encode("iso-8859-1")
|
||||
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
handle, "%s\%s" % (TZKEYNAME, self._stdname)) as tzkey:
|
||||
_keydict = valuestodict(tzkey)
|
||||
self._display = _keydict["Display"]
|
||||
except OSError:
|
||||
self._display = None
|
||||
|
||||
self._stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||
self._dstoffset = self._stdoffset-keydict["DaylightBias"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[1:6]
|
||||
|
||||
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[1:6]
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, ())
|
||||
|
||||
|
||||
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||
"""dayofweek == 0 means Sunday, whichweek 5 means last instance"""
|
||||
first = datetime.datetime(year, month, 1, hour, minute)
|
||||
weekdayone = first.replace(day=((dayofweek-first.isoweekday()) % 7+1))
|
||||
for n in range(whichweek):
|
||||
dt = weekdayone+(whichweek-n)*ONEWEEK
|
||||
if dt.month == month:
|
||||
return dt
|
||||
|
||||
|
||||
def valuestodict(key):
|
||||
"""Convert a registry key's values to a dictionary."""
|
||||
dict = {}
|
||||
size = winreg.QueryInfoKey(key)[1]
|
||||
for i in range(size):
|
||||
data = winreg.EnumValue(key, i)
|
||||
dict[data[0]] = data[1]
|
||||
return dict
|
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,21 @@ UMASK = None
|
||||
|
||||
POLLING_FAILOVER = False
|
||||
|
||||
DEV = 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 +105,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 +122,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 +188,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 +198,6 @@ def initialize(config_file):
|
||||
_INITIALIZED = True
|
||||
return True
|
||||
|
||||
|
||||
def daemonize():
|
||||
if threading.activeCount() != 1:
|
||||
logger.warn(
|
||||
@@ -282,10 +293,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 +306,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 +340,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 +353,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 +368,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)
|
||||
|
||||
|
||||
@@ -376,8 +396,8 @@ 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, '
|
||||
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, '
|
||||
'transcode_key TEXT, 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 +420,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 +450,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 +621,24 @@ 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 sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT transcode_key FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN transcode_key TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT reference_id FROM session_history')
|
||||
@@ -651,6 +689,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')
|
||||
@@ -689,32 +742,55 @@ def dbcheck():
|
||||
|
||||
# Upgrade notify_log table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT on_pause FROM notify_log')
|
||||
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 on_pause INTEGER'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE notify_log ADD COLUMN on_resume INTEGER'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE notify_log ADD COLUMN on_buffer INTEGER'
|
||||
'ALTER TABLE notify_log ADD COLUMN poster_url TEXT'
|
||||
)
|
||||
|
||||
# Upgrade notify_log table from earlier versions
|
||||
# Upgrade notify_log table from earlier versions (populate table with data from notify_log)
|
||||
try:
|
||||
c_db.execute('SELECT on_created FROM notify_log')
|
||||
c_db.execute('SELECT timestamp 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 on_created INTEGER'
|
||||
'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 +815,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 +823,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 +860,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 +877,7 @@ def dbcheck():
|
||||
conn_db.commit()
|
||||
c_db.close()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False):
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
@@ -818,10 +906,17 @@ def shutdown(restart=False, update=False):
|
||||
if '--nolaunch' not in args:
|
||||
args += ['--nolaunch']
|
||||
logger.info('Restarting PlexPy with %s', 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,6 +57,8 @@ 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()))
|
||||
|
||||
# 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()
|
||||
@@ -77,11 +79,14 @@ 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())
|
||||
|
||||
# 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()
|
||||
@@ -91,6 +96,7 @@ class ActivityHandler(object):
|
||||
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,6 +115,8 @@ class ActivityHandler(object):
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
# 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()
|
||||
@@ -129,6 +137,8 @@ class ActivityHandler(object):
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
# 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()
|
||||
@@ -156,9 +166,12 @@ 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())
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -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
|
||||
# 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':
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=db_session, notify_action='watched')).start()
|
||||
# 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,6 +38,9 @@ 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.")
|
||||
|
||||
# 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()
|
||||
@@ -58,12 +61,20 @@ def check_active_sessions(ws_request=False):
|
||||
# Here we can check the play states
|
||||
if session['state'] != stream['state']:
|
||||
if session['state'] == 'paused':
|
||||
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':
|
||||
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,
|
||||
@@ -105,6 +116,10 @@ def check_active_sessions(ws_request=False):
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
# 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:
|
||||
@@ -119,11 +134,16 @@ def check_active_sessions(ws_request=False):
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
# 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,6 +152,8 @@ 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,
|
||||
@@ -139,30 +161,52 @@ def check_active_sessions(ws_request=False):
|
||||
|
||||
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']])
|
||||
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()
|
||||
|
||||
# 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='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,6 +215,8 @@ def check_active_sessions(ws_request=False):
|
||||
% str(int_ping_count))
|
||||
|
||||
if int_ping_count == 3:
|
||||
# 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()
|
||||
@@ -225,6 +271,9 @@ 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']))
|
||||
|
||||
# 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()
|
||||
@@ -243,6 +292,9 @@ 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']))
|
||||
|
||||
# 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()
|
||||
@@ -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.")
|
||||
|
||||
# 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:
|
||||
# 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
|
||||
@@ -29,6 +29,7 @@ class ActivityProcessor(object):
|
||||
def write_session(self, session=None, notify=True):
|
||||
if session:
|
||||
values = {'session_key': session['session_key'],
|
||||
'transcode_key': session['transcode_key'],
|
||||
'section_id': session['section_id'],
|
||||
'rating_key': session['rating_key'],
|
||||
'media_type': session['media_type'],
|
||||
@@ -78,9 +79,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,17 +99,24 @@ 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
|
||||
|
||||
section_id = session['section_id'] if not is_import else import_metadata['section_id']
|
||||
|
||||
if not is_import:
|
||||
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)
|
||||
|
||||
# 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 +125,13 @@ 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',
|
||||
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,16 @@ class ActivityProcessor(object):
|
||||
|
||||
return None
|
||||
|
||||
def set_session_state(self, session_key=None, state=None, view_offset=0):
|
||||
if str(session_key).isdigit() and str(view_offset).isdigit():
|
||||
values = {'view_offset': int(view_offset)}
|
||||
def set_session_state(self, session_key=None, state=None, **kwargs):
|
||||
if str(session_key).isdigit():
|
||||
values = {}
|
||||
|
||||
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
@@ -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))
|
@@ -58,3 +58,15 @@ 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,12 +104,14 @@ _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),
|
||||
'GET_FILE_SIZES_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
|
||||
'GIT_BRANCH': (str, 'General', 'master'),
|
||||
'GIT_PATH': (str, 'General', ''),
|
||||
'GIT_TOKEN': (str, 'General', ''),
|
||||
'GIT_USER': (str, 'General', 'drzoidberg33'),
|
||||
'GRAPH_TYPE': (str, 'General', 'plays'),
|
||||
'GRAPH_DAYS': (int, 'General', 30),
|
||||
@@ -121,14 +131,19 @@ _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_ENVIRONMENT': (str, 'General', 'production'),
|
||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||
'HTTP_PASSWORD': (str, 'General', ''),
|
||||
'HTTP_PORT': (int, 'General', 8181),
|
||||
@@ -151,8 +166,10 @@ _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_BLACKLIST': (int, 'General', 1),
|
||||
'LOG_DIR': (str, 'General', ''),
|
||||
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
||||
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||
@@ -165,6 +182,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 +200,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 +229,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 +245,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 +261,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 +276,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 +290,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 +306,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 +324,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 +333,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 +346,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 +360,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 +372,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 +388,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 +411,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,9 +430,13 @@ _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)
|
||||
}
|
||||
|
||||
_BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID']
|
||||
_WHITELIST_KEYS = ['HTTPS_KEY', 'UPDATE_SECTION_IDS']
|
||||
|
||||
|
||||
# pylint:disable=R0902
|
||||
# it might be nice to refactor for fewer instance variables
|
||||
@@ -408,6 +450,19 @@ class Config(object):
|
||||
for key in _CONFIG_DEFINITIONS.keys():
|
||||
self.check_setting(key)
|
||||
self._upgrade()
|
||||
self._blacklist()
|
||||
|
||||
def _blacklist(self):
|
||||
""" Add tokens and passwords to blacklisted words in logger """
|
||||
blacklist = []
|
||||
|
||||
for key, subkeys in self._config.iteritems():
|
||||
for subkey, value in subkeys.iteritems():
|
||||
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
||||
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
|
||||
blacklist.append(value.strip())
|
||||
|
||||
plexpy.logger._BLACKLIST_WORDS = blacklist
|
||||
|
||||
def _define(self, name):
|
||||
key = name.upper()
|
||||
@@ -467,6 +522,8 @@ class Config(object):
|
||||
except IOError as e:
|
||||
plexpy.logger.error("Error writing configuration file: %s", e)
|
||||
|
||||
self._blacklist()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Returns something from the ini unless it is a real property
|
||||
@@ -511,6 +568,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:
|
||||
@@ -525,3 +583,19 @@ class Config(object):
|
||||
home_library_cards.remove('library_statistics')
|
||||
self.HOME_LIBRARY_CARDS = home_library_cards
|
||||
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'
|