Compare commits

...

535 Commits

Author SHA1 Message Date
ee2bb22dc9 README: Jellyfin Login ✓ 2021-02-06 18:42:24 +01:00
3852275b74 Jellyfin Login ✓ 2021-02-06 18:41:24 +01:00
6e1067e43e remodel config to match jellyfin purpose 2021-02-06 16:36:15 +01:00
110baa67d5 added Status to README.md, some more renaming 2021-02-06 14:40:22 +01:00
3fb7f4ddd1 more removal of cloud services, progress on setup wizard 2021-02-06 00:21:58 +01:00
38f0a44fa0 removed plextv / pms, added TODO's for reimplementing Jellyfin 2021-02-05 21:01:43 +01:00
af3c826f7d added req. for easy install 2021-02-05 19:36:57 +01:00
cbee080b54 Got it to run, renamed some template stuff
Now onto the Jellyfin API, yay!
2021-02-05 19:32:27 +01:00
ecbf38c005 Overhauled README for JellyPy 2021-02-05 18:32:51 +01:00
91e28df1a5 Removed some facebook stuff, maybe adding it back later 2021-02-05 18:16:12 +01:00
1df28243c3 More rename, more -python2 2021-02-05 17:29:23 +01:00
b867dc9be2 Rename; Removed deps from repo 2021-02-05 16:30:46 +01:00
JonnyWong16
97f80adf0b v2.6.5 2021-01-09 14:24:27 -08:00
JonnyWong16
2fc7b08909 Add misfire grace time to scheduled tasks 2021-01-09 14:15:32 -08:00
JonnyWong16
defceed696 Fix homepage server status when sections are disabled 2021-01-06 17:24:53 -08:00
JonnyWong16
249533ac51 Migrate snap user data 2021-01-05 17:56:53 -08:00
JonnyWong16
12aee8762e Allow snap package to access home directory 2021-01-05 10:19:41 -08:00
JonnyWong16
d9325b7adf Add Crypto link to funding 2020-12-28 14:16:16 -08:00
JonnyWong16
4975cad4fa Keep news open for longer 2020-12-27 14:53:33 -08:00
JonnyWong16
33fc079318 More logger blacklist improvements 2020-12-27 14:52:18 -08:00
JonnyWong16
b3b2752554 Fix regex masking in logger args 2020-12-27 13:15:06 -08:00
JonnyWong16
505cf25ca3 Add back crypto donation 2020-12-25 14:01:04 -08:00
JonnyWong16
9747e3ba98 Add pull request workflow 2020-12-24 18:23:06 -08:00
JonnyWong16
729191722a Fix typo 2020-12-24 17:00:39 -08:00
JonnyWong16
ff2cf73f23 Update .gitignore 2020-12-24 16:43:00 -08:00
JonnyWong16
9c4d97c0f8 Update installer workflow 2020-12-24 16:43:00 -08:00
JonnyWong16
be911e7700 Move nsis plugin folder 2020-12-24 16:42:37 -08:00
JonnyWong16
00629c0983 Use updater to modify Task Scheduler XML 2020-12-24 16:41:28 -08:00
JonnyWong16
52ebc9a908 Move Tautulli to Program Files x64 2020-12-24 16:41:21 -08:00
JonnyWong16
a029d6a931 Restart Tautulli process if Windows installer failed 2020-12-22 19:26:53 -08:00
JonnyWong16
7641e3b081 Add logger function to Windows updater 2020-12-22 19:18:45 -08:00
JonnyWong16
b54210480f Don't shutdown for Windows installer update 2020-12-22 19:09:27 -08:00
JonnyWong16
0d9c1c640e Update Windows updater 2020-12-22 19:09:06 -08:00
JonnyWong16
7f84353c69 Disable console window when running task scheduler 2020-12-22 16:24:21 -08:00
JonnyWong16
c319a4a5cc Don't auto restart when updating Windows install 2020-12-22 16:22:45 -08:00
JonnyWong16
60f13df992 Add auto update for Windows exe install 2020-12-22 15:59:19 -08:00
JonnyWong16
dea51e32a5 Strip branch and version when reading text file in Windows updater 2020-12-22 15:46:11 -08:00
JonnyWong16
7019f5618b Update Windows powershell command to move updater 2020-12-22 15:36:21 -08:00
JonnyWong16
9106c068ac Update Windows installer workflow 2020-12-22 15:26:37 -08:00
JonnyWong16
0b845294fb Add Tautulli Windows exe updater 2020-12-22 15:21:06 -08:00
JonnyWong16
7e850dd88d Fix typo in apscheduler in package requirements file 2020-12-22 12:33:52 -08:00
JonnyWong16
877bf7060e Missing = sign in package requirements file 2020-12-22 12:31:22 -08:00
JonnyWong16
9326d03a57 Update PyInstaller to 4.1
* APScheduler needs to be installed from PyPI due to the way it retrieves it's own version number using pkg_resources.get_distribution
2020-12-22 12:29:43 -08:00
JonnyWong16
4787f42d2e Add args to Windows and MacOS auto startup 2020-12-21 13:39:53 -08:00
JonnyWong16
56a9ccd818 Make Windows registry key unique to allow more than one instance 2020-12-21 13:39:29 -08:00
JonnyWong16
1019fecc9e v2.6.4 2020-12-20 08:59:41 -08:00
JonnyWong16
1855f93c1c Revert Snap data folder
* Fixes Tautulli/Tautulli-Issues#294
2020-12-20 08:58:02 -08:00
JonnyWong16
52e6a44aa4 Put Discord notification after release 2020-12-19 16:18:29 -08:00
JonnyWong16
0b77808af6 Fix release changelog 2020-12-19 16:18:13 -08:00
JonnyWong16
9233ed5c53 Revert "Also publish snap to beta for full release"
This reverts commit 366823cee9.
2020-12-19 15:56:09 -08:00
JonnyWong16
ee68c0f622 Fix release installer path 2020-12-19 15:42:23 -08:00
JonnyWong16
366823cee9 Also publish snap to beta for full release 2020-12-19 15:30:02 -08:00
JonnyWong16
40e1eb9a49 Revert "Build snap based on branch instead of tag"
This reverts commit 2e5dd05a6c.
2020-12-19 15:22:30 -08:00
JonnyWong16
1af419a860 v2.6.3 2020-12-19 15:09:56 -08:00
JonnyWong16
397f18c435 Disable updater if using Python 2 2020-12-19 14:59:05 -08:00
JonnyWong16
2e5dd05a6c Build snap based on branch instead of tag 2020-12-19 12:11:03 -08:00
JonnyWong16
a9fb8ddfb8 Separate Discord notification jobs 2020-12-18 14:31:15 -08:00
JonnyWong16
562c726787 Merge pull request #1385 from elpollodiablo/master
Change http_handler to use requests instead of urllib3
2020-12-18 14:11:53 -08:00
JonnyWong16
5f82c1dc17 Add python3-pycryptodome to snap package 2020-12-17 20:48:23 -08:00
JonnyWong16
222800bdb6 Update README with Snap badges 2020-12-17 20:27:41 -08:00
JonnyWong16
5dd3636571 Fix typo in Snap architecture 2020-12-17 19:02:00 -08:00
JonnyWong16
2296a9fbb3 Merge pull request #1375 from capruro/master
Snap Package integration
2020-12-17 18:53:25 -08:00
JonnyWong16
63b5a7c036 Update workflows 2020-12-17 18:46:02 -08:00
JonnyWong16
b74ca2670e Update Publish Snap workflow 2020-12-17 18:44:29 -08:00
JonnyWong16
393f4e0e58 Add multi-architecture build to Publish Snap workflow 2020-12-17 18:38:04 -08:00
JonnyWong16
3a9ca29e99 Add pull request to workflows 2020-12-17 18:28:50 -08:00
JonnyWong16
32995fef24 Update snapcraft.yml 2020-12-17 17:47:28 -08:00
JonnyWong16
a73c99fc64 Disable updated for Snap package 2020-12-17 17:47:28 -08:00
JonnyWong16
a5834470ba Update Publish Docker workflow 2020-12-17 17:41:12 -08:00
JonnyWong16
da3bc127dc Add TAUTULLI_SNAP environment variable 2020-12-17 12:48:12 -08:00
JonnyWong16
0dddc4d58f Fix typo in Publish Snap workflow 2020-12-16 17:56:39 -08:00
JonnyWong16
a4d5d9157b Add snap to .dockerignore 2020-12-16 17:49:17 -08:00
JonnyWong16
c70d5d4398 Add Publish Snap GitHub workflow 2020-12-16 17:49:17 -08:00
JonnyWong16
7c08b07ef5 Update snapcraft.yaml 2020-12-16 17:49:17 -08:00
capruro
e426b5dd35 add PyOpenSSL package 2020-12-16 17:46:50 -08:00
capruro
2fdf619582 Delete .editorconfig
NOT required to be implemented :)
2020-12-16 17:46:50 -08:00
Marcello Franco
d9eed14b7a Initial commit 2020-12-16 17:46:40 -08:00
JonnyWong16
8230ffb8a4 Update readme installer build badges 2020-12-16 00:01:00 -08:00
JonnyWong16
7098930b19 Update build installer workflow with matrix 2020-12-15 23:54:30 -08:00
JonnyWong16
56244245a4 Fix export accessible and exists for media info level 9 2020-12-15 22:49:51 -08:00
JonnyWong16
dd2f12fa8e Fix typo in notifier error log message 2020-12-15 22:49:51 -08:00
JonnyWong16
9598247a0d Merge pull request #1384 from krowvin/patch-1
Update reference link to CherryPy auth tool
2020-12-15 22:48:46 -08:00
JonnyWong16
230ee90b1c Remove link to web.archive.org for CherryPy auth tool 2020-12-15 22:47:11 -08:00
JonnyWong16
e705bedc91 Clean up http_handler using requests
* Restore ThreadPool for multiple requests
* Use a requests.Session()
* Update requests.exceptions
2020-12-15 22:38:43 -08:00
JonnyWong16
b5ebe7590c Create pull_request_template.md 2020-12-12 12:16:46 -08:00
Philip Poten
6d0831ceaa replace urllib3 with requests, remove unused session logic 2020-12-12 16:03:18 +01:00
JonnyWong16
19e00ee2f2 Set template icon for macOS menu bar 2020-12-06 11:13:21 -08:00
JonnyWong16
80723d224e Add username to masked session info for guests 2020-12-05 18:44:06 -08:00
JonnyWong16
0c82bb023a v2.6.2 2020-12-05 10:33:13 -08:00
JonnyWong16
0a86f24095 Fix plexapi.library.Collections.children()
* Fixes Tautulli/Tautulli-Issues#290
2020-12-04 19:16:40 -08:00
JonnyWong16
b41249cfa8 Put update status in tray menu 2020-12-04 18:48:39 -08:00
JonnyWong16
6659802689 Remove tray update icon 2020-12-04 18:30:56 -08:00
JonnyWong16
964c503223 Add flat white logo and icon 2020-12-04 17:54:15 -08:00
JonnyWong16
15568bf20a Pin macOS requirement pyobjc-core==6.2.2 2020-12-04 13:28:51 -08:00
JonnyWong16
d10cd324bb Pin requirements dependencies versions 2020-12-02 20:17:39 -08:00
JonnyWong16
2a22ab8c33 Remove LSBackgroundOnly from macOS app plist file 2020-11-27 21:43:48 -08:00
JonnyWong16
ca736cdae2 Remove accessible and exists from media info export level 2
* Only reload export object with checkFiles=True if accessible and exists are requested.
2020-11-27 21:31:21 -08:00
JonnyWong16
d589c57dd2 Update plexapi 3.6.0-tautulli 2020-11-27 21:07:30 -08:00
JonnyWong16
9b0caf2a47 Fix export custom child fields failing without included parent field 2020-11-27 20:33:04 -08:00
JonnyWong16
f8b00bbd67 codecID only for video streams and remove dialogNorm from audio streams 2020-11-25 22:13:49 -08:00
JonnyWong16
91a8c0e7a0 Add transient to subtitle stream export 2020-11-25 21:46:24 -08:00
JonnyWong16
2089172384 Add streamIdentifier to audio stream export 2020-11-25 21:27:58 -08:00
JonnyWong16
1ab87e5334 Enable high resolution in macOS app 2020-11-21 21:04:22 -08:00
JonnyWong16
b5e6861032 Update certifi 2020.11.08 2020-11-21 15:05:58 -08:00
JonnyWong16
189930918a Fix entering / in Public Tautulli Domain setting 2020-11-15 11:29:11 -08:00
krowvin
ff1bd0a4b8 Update webauth.py
Link is broken. Added two alternate links to the same information.
2020-11-12 17:03:04 -06:00
JonnyWong16
e544d0dd07 Improve login rate limit 2020-11-09 09:43:09 -08:00
JonnyWong16
3e0b240154 Add rate limiting to login page 2020-11-08 15:36:40 -08:00
JonnyWong16
199119cafb Reword notification settings help text 2020-11-08 13:17:14 -08:00
JonnyWong16
89ab665923 Add user new device notification setting for initial stream only 2020-11-08 13:16:49 -08:00
JonnyWong16
dfb60de6d2 Add machine_id to get_history API response 2020-11-08 12:34:03 -08:00
JonnyWong16
da8d41868d v2.6.1 2020-11-03 17:51:38 -08:00
JonnyWong16
e9db43ebf6 Remove tqdm 2020-11-02 23:06:59 -08:00
JonnyWong16
c0453eae47 Fix unique img_hash in database 2020-11-02 19:49:33 -08:00
JonnyWong16
a8863a5aeb Remove cherrypy engine log filter 2020-11-02 18:39:50 -08:00
JonnyWong16
a8adad7dbb v2.6.0 2020-10-31 17:05:51 -07:00
JonnyWong16
4cfa5ac10b Remove encoding from Growl message body 2020-10-30 21:37:30 -07:00
JonnyWong16
55090ddeaa Clean up start.sh 2020-10-30 21:30:28 -07:00
JonnyWong16
14346b0e69 Improve Docker exec user 2020-10-30 21:27:39 -07:00
JonnyWong16
ac24acf9ce Publish Docker image to GitHub Container Registry 2020-10-29 21:44:27 -07:00
JonnyWong16
4cde62fde9 Update Android platform icon 2020-10-27 18:34:21 -07:00
JonnyWong16
7489bc8d98 Merge pull request #1383 from zheileman/apple-data-detectors
Fix styling of Apple data-detectors in newsletters
2020-10-25 14:00:15 -07:00
JonnyWong16
cde9287d85 Update favicon to circle logo 2020-10-25 13:42:00 -07:00
JonnyWong16
558023e18e Improve startup speed by refreshing on a separate thread 2020-10-25 13:07:42 -07:00
JonnyWong16
8157ee7811 Cache GitHub update check on startup
* Fixes Tautulli/Tautulli-Issues#184
2020-10-25 11:39:48 -07:00
JonnyWong16
d746d2913f Fix mobile device table migration 2020-10-25 10:51:32 -07:00
JonnyWong16
0136fc6436 Update plexapi.LibrarySection subclasses 2020-10-23 23:29:53 -07:00
JonnyWong16
7ce280cb92 Fix ratings on info page for new Plex Movie agent 2020-10-23 17:51:48 -07:00
JonnyWong16
0209fa87aa Update rating notification parameters for new Plex Movie agent 2020-10-23 17:46:11 -07:00
JonnyWong16
62cc2f769f Fix docker build args 2020-10-21 19:37:14 -07:00
JonnyWong16
a49d44c880 Add logger message for missing server identifier when refreshing users 2020-10-21 19:33:38 -07:00
JonnyWong16
dab288380a Change jquery .width to .css for activity progress bar
* For some reason jquery 3.5 isn't accepting `.width(progress + '%')`
2020-10-21 14:26:05 -07:00
Jesus Laiz
2ac5c35065 Fix styling of Apple data-detectors in newsletters
The existing style was not properly targetting the links Apple inject when (wrongly, in this case) detecting phone numbers in newsletters.

This has no effect in any other platform or device.
The numbers are still clickable, couldn't fine a way to disable the functionality completely (tried the `format-detection` meta tag with no luck), but at least the styles are not changed anymore.

I tested this on iPhone and iPad and you can see how it looks before and after the change below.
2020-10-21 11:47:04 +01:00
JonnyWong16
ec9e2fe0f0 Patch plexapi.library.Collections to PlexPartialObject 2020-10-20 15:49:29 -07:00
JonnyWong16
ecbe79b5b9 Add intro markers to exporter 2020-10-19 09:23:50 -07:00
JonnyWong16
c4ac03738b Add plexapi.media.Marker to plexapi.video.Episode 2020-10-19 09:21:40 -07:00
JonnyWong16
352dbd9bc8 Update brand logo colours 2020-10-17 21:25:17 -07:00
JonnyWong16
393b395df0 Add delete_synced_item to the API 2020-10-16 19:49:34 -07:00
JonnyWong16
1a96da04a1 Add sync_id parameter to get_metadata 2020-10-16 14:03:02 -07:00
JonnyWong16
615b98955a v2.6.0-beta 2020-10-16 13:27:04 -07:00
JonnyWong16
11b2b67f9d Remove set-env from release workflow 2020-10-16 13:18:52 -07:00
JonnyWong16
0e44255e6a Export photo locations 2020-10-16 12:31:34 -07:00
JonnyWong16
a649d2ec12 Add locations for plexapi.photo.Photo and plexapi.video.Clip 2020-10-16 12:31:18 -07:00
JonnyWong16
8a953e789c Add all locations to m3u8 playlists 2020-10-16 11:47:44 -07:00
JonnyWong16
317d32eb0c Merge pull request #1381 from MichaIng/patch-1
Avoid daemon forking with systemd
2020-10-16 11:40:14 -07:00
JonnyWong16
b2ddedc0ae Update API register device doc string 2020-10-15 22:47:56 -07:00
JonnyWong16
620d2cf730 Merge pull request #1382 from dotsam/margin-fix-better
Fix margins on dashboard cards
2020-10-15 22:47:21 -07:00
JonnyWong16
2578592cc7 Move ratingKey, title, and titleSort to front of csv headers 2020-10-15 22:10:42 -07:00
JonnyWong16
44c643d7da Add progress percent to export table 2020-10-15 21:56:20 -07:00
JonnyWong16
39d6edd581 Fix memory leak in exporter 2020-10-15 21:19:42 -07:00
JonnyWong16
5a14b5bc35 Add all Plex server and Tautulli info to register device response 2020-10-15 11:16:15 -07:00
JonnyWong16
82d9719eee Fix default library export type 2020-10-15 10:56:28 -07:00
JonnyWong16
401b75a76b Update docker build workflow 2020-10-15 10:15:08 -07:00
JonnyWong16
ab75628cf7 Make export threads advanced config setting 2020-10-14 23:48:58 -07:00
JonnyWong16
57d08e231c Fix typo in plexapi includeBandwidths 2020-10-14 23:25:41 -07:00
JonnyWong16
f6b800c372 Remove horizontal rule in exporter docs 2020-10-14 20:01:28 -07:00
JonnyWong16
26773ac67f Reword export individual files in export modal 2020-10-14 20:01:10 -07:00
JonnyWong16
5b63cb38ae Timestamp required for checking if export exists 2020-10-14 14:13:55 -07:00
JonnyWong16
30b655a32a Refactor saving xml and m3u8 files 2020-10-14 14:02:08 -07:00
JonnyWong16
f3fa9601c0 Rework exporter to allow exporting individual files from library 2020-10-14 12:48:08 -07:00
JonnyWong16
034ad05383 Add some more metadata to m3u8 exports 2020-10-13 18:48:21 -07:00
JonnyWong16
7b936fd664 Add include_activity parameter to get_history API 2020-10-13 16:26:35 -07:00
JonnyWong16
a120f52e0d Add grandparentTitle to m3u8 export 2020-10-13 09:15:45 -07:00
Sam Edwards
58f2d22ef4 Swap left/right margin on cards and make cards fit better on mobile 2020-10-12 23:55:13 -07:00
JonnyWong16
962777284a Add collectionSort value for custom sort 2020-10-12 21:32:16 -07:00
JonnyWong16
d4c8066209 Cast plexapi.Collections.collectionMode and collectionSort to int 2020-10-12 21:31:46 -07:00
JonnyWong16
9a9db88efd Add Dolby vision to export attributes 2020-10-12 21:13:29 -07:00
JonnyWong16
f5ad9cfe14 Add Dolby vision attributes to plexapi.media.VideoStream 2020-10-12 21:13:10 -07:00
JonnyWong16
d268a7aa23 Add back in tautulli_version to register device 2020-10-12 14:33:43 -07:00
JonnyWong16
1896239bd3 Add link to exporter guide in export modal 2020-10-12 13:08:53 -07:00
JonnyWong16
07c71750d5 Move API docs to the wiki 2020-10-12 12:53:16 -07:00
JonnyWong16
d5171109f5 Add anchor to children / item in exporter docs 2020-10-12 12:09:49 -07:00
JonnyWong16
36aa795c52 Add min_version parameter to register_device API command 2020-10-12 11:56:55 -07:00
JonnyWong16
7914f56ec3 Always close mobile device QR modal on successful registration 2020-10-12 11:54:36 -07:00
JonnyWong16
a39c6c1047 Improve sorting of json and xml export attributes 2020-10-11 16:03:16 -07:00
JonnyWong16
270e07341a Add m3u8 to export API docs 2020-10-11 14:53:09 -07:00
JonnyWong16
da7c66f414 Add stat_id and stats_start options to get_home_stats API 2020-10-11 14:31:34 -07:00
JonnyWong16
d97b87d9cc Allow deleting mobile device using the registered device_id 2020-10-11 13:55:50 -07:00
JonnyWong16
bee3361ace Return Tautulli version in register device API 2020-10-11 13:54:24 -07:00
JonnyWong16
276ea4dd98 Only build tables for exporter docs 2020-10-10 10:06:25 -07:00
JonnyWong16
dd45b47032 Filter out Cherrypy engine serving log message 2020-10-10 10:05:06 -07:00
JonnyWong16
1fd4ec3ca3 Add moment-duration-format.js plugin for graphs 2020-10-10 09:49:29 -07:00
JonnyWong16
1e807af2d4 Fix graphs tab not saving 2020-10-10 09:48:55 -07:00
JonnyWong16
88a5db05b7 Move file format to top of export modal 2020-10-09 01:24:21 -07:00
JonnyWong16
e6c8bd0c13 Add auto-generation of exporter docs 2020-10-09 00:49:17 -07:00
JonnyWong16
3be9c84f2b Add attribute sort helper function 2020-10-08 19:53:57 -07:00
JonnyWong16
881f37f731 Change default sig of human duration function 2020-10-08 19:53:32 -07:00
JonnyWong16
4d37f2bab2 Add none to export level 0 description 2020-10-08 16:14:48 -07:00
JonnyWong16
3217c2da0b Enable export of season/album artwork if selected 2020-10-08 13:33:51 -07:00
JonnyWong16
280ae04b3d Set art to None for plexapi.video.Season/Episode 2020-10-08 13:32:25 -07:00
JonnyWong16
d5705a52e9 Use collection.title if collection.sortTitle is None 2020-10-08 12:41:47 -07:00
JonnyWong16
43ab2f22a8 Update wording on image export levels 2020-10-08 11:57:41 -07:00
JonnyWong16
8bb40036bc Update API docs for export thumb_level and art_level 2020-10-08 11:21:58 -07:00
JonnyWong16
8ee934404f Reword image levels in export modal 2020-10-08 11:21:39 -07:00
JonnyWong16
aa9dbafa28 Check images exported after export 2020-10-08 11:13:00 -07:00
JonnyWong16
2b8fea8bf8 Allow addition custom export image fields 2020-10-08 11:12:09 -07:00
JonnyWong16
985f4293b3 Export images based on level 2020-10-08 10:12:28 -07:00
JonnyWong16
22b162b3c4 Cast plexapi.media.Poster selected to bool 2020-10-08 10:12:28 -07:00
JonnyWong16
7f3d8cfb8d Change thumb_level and art_level to int 2020-10-08 10:12:28 -07:00
JonnyWong16
9a7627e35e Rename include_thumb/art to thumb/art_level 2020-10-08 09:14:37 -07:00
JonnyWong16
f141c67ceb Load key before _details_key for plexapi.audio.Artist 2020-10-07 22:01:01 -07:00
JonnyWong16
e4372644e1 Better csv fields sorting 2020-10-07 20:42:04 -07:00
JonnyWong16
8552b00be4 Always try to delete export images folder 2020-10-07 20:42:04 -07:00
JonnyWong16
978fea5dde Hide collections tab for music libraries 2020-10-07 18:50:22 -07:00
JonnyWong16
b3eeaeeda5 Don't return the main child attribute as a custom field 2020-10-07 18:29:16 -07:00
JonnyWong16
0f2ac5104e Fix parsing custom fields 2020-10-07 18:25:48 -07:00
JonnyWong16
f7766fff14 Log cherrypy errors 2020-10-07 00:06:23 -07:00
JonnyWong16
b7c2e42190 Fix overflow in track list/playlist 2020-10-06 21:24:35 -07:00
JonnyWong16
56472f8dd5 Fix loading custom fields for show/artist 2020-10-06 21:18:11 -07:00
JonnyWong16
969934b8c0 Update bootstrap.min.css 2020-10-04 19:42:30 -07:00
JonnyWong16
62f153acd2 Update jquery on other pages 2020-10-04 19:37:17 -07:00
JonnyWong16
b53f16645c Set Patreon image width 2020-10-04 16:52:37 -07:00
JonnyWong16
6c2786dd78 Add noreferrer to all external links 2020-10-04 16:52:37 -07:00
JonnyWong16
64a9b0e622 Update bootstrap to v3.3.7
* Partially addresses Tautulli/Tautulli-Issues#127. Updating to bootstrap 4 requires a major rewrite.
2020-10-04 16:30:11 -07:00
JonnyWong16
3d05a74ef4 Update moment.js to 2.29.0
* Addresses Tautulli/Tautulli-Issues#127
2020-10-04 16:17:04 -07:00
JonnyWong16
3a1c92944f Update navigation tabs to work with jquery update 2020-10-04 16:10:51 -07:00
JonnyWong16
6b34b82f52 Update jquery to 3.5.1
* Fixes Tautulli/Tautulli-Issues#280
* Addresses Tautulli/Tautulli-Issues#127
2020-10-04 16:07:55 -07:00
JonnyWong16
322c090d8a Automatically show HTTP root in Public Tautulli Domain input box 2020-10-04 14:15:06 -07:00
JonnyWong16
4bb49f9836 Add jquery InputAffix 2020-10-04 14:14:20 -07:00
JonnyWong16
20566168a1 Improve photoalbum export 2020-10-04 13:01:05 -07:00
JonnyWong16
03bf4a9ef8 Add default stream state icon 2020-10-04 12:11:05 -07:00
JonnyWong16
842a76aae1 Update dict_to_xml helper function 2020-10-04 01:35:11 -07:00
JonnyWong16
e3214946a3 Fix exporter for photo albums and clips 2020-10-04 01:21:44 -07:00
JonnyWong16
36f877c7ff Update plexapi.video.Clip and plexapi.photo.Photoalbum 2020-10-04 01:18:05 -07:00
JonnyWong16
6e41b7ef3d Hide tab bar on info page for guests 2020-10-03 11:29:28 -07:00
JonnyWong16
1fc9a9bcea Add downloads badge to readme 2020-10-03 10:57:42 -07:00
JonnyWong16
3a439cb81c Update API docs 2020-10-03 10:29:50 -07:00
JonnyWong16
e8b0de0320 Improve image export help text on export modal 2020-10-03 10:27:40 -07:00
JonnyWong16
4d033bb379 Add custom fields for manually exporting season/episode images 2020-10-03 10:04:43 -07:00
JonnyWong16
fffd1ffda3 Fix missing select columns button for user IP address table 2020-10-02 23:41:16 -07:00
JonnyWong16
151f23fd92 Move common buttons to nav bar on info page 2020-10-02 23:34:30 -07:00
JonnyWong16
25572d6a5b Change key for plexapi.LibrarySection.collection 2020-10-02 23:25:50 -07:00
JonnyWong16
e27efb3946 Add kwargs to plexapi playlists fetchItems 2020-10-02 23:25:23 -07:00
JonnyWong16
ca69293d8b Fix footer text on collections and playlists tables 2020-10-02 23:19:36 -07:00
JonnyWong16
f7fa773ec7 Add thumb popover to collections and playlists tables 2020-10-02 22:03:33 -07:00
JonnyWong16
f84c4ca73c Add thumb popover to lists on info page 2020-10-02 21:58:13 -07:00
JonnyWong16
f9d828ea67 Fix import statement 2020-10-02 21:35:23 -07:00
JonnyWong16
739c977cd7 Merge branch 'v2.5-export' into nightly 2020-10-02 20:45:11 -07:00
JonnyWong16
14b98a32e0 v2.5.6 2020-10-02 20:35:06 -07:00
JonnyWong16
34c9ede9c9 Add m3u8 export file format 2020-10-02 19:42:24 -07:00
JonnyWong16
be9f06795d Fix loading playlists for user not on Plex server 2020-10-02 13:05:14 -07:00
JonnyWong16
ea9904bd56 Add playlist export for users 2020-10-02 12:54:07 -07:00
JonnyWong16
501f08dd5e Add Playlist tab to user page 2020-10-02 11:57:13 -07:00
JonnyWong16
a985cec9c2 Fix loading synced items for guest access 2020-10-02 11:16:49 -07:00
JonnyWong16
c8b0ff22f6 Update guest access for collection and playlist changes 2020-10-02 11:11:15 -07:00
JonnyWong16
3cc8c1f8c5 Fix loading Live TV library page 2020-10-02 10:27:59 -07:00
JonnyWong16
5e8b946571 Improve nav pills css 2020-10-02 10:27:45 -07:00
JonnyWong16
dd4c0d24b7 Add tabs for history and export on info pages 2020-10-02 10:14:00 -07:00
JonnyWong16
fc39f1521d Change library and user tabs to nav pills 2020-10-02 10:13:41 -07:00
JonnyWong16
60cadb1e11 Go to export tab after exporting 2020-10-02 09:56:30 -07:00
JonnyWong16
28c745c19c Patch DataTables ColVis to fix dropdown extending past bottom of page 2020-10-02 00:22:19 -07:00
JonnyWong16
900b524672 Add hidden Custom Fields column to export table 2020-10-01 21:42:15 -07:00
JonnyWong16
627129dd95 Improve XML export 2020-10-01 21:13:07 -07:00
JonnyWong16
8d18e98ca7 Fix pms_image_proxy for composites 2020-10-01 20:22:52 -07:00
JonnyWong16
0ba755e463 Make collection titles searchable in table 2020-10-01 19:43:10 -07:00
JonnyWong16
72215a9f44 Update library export API docs 2020-10-01 16:15:00 -07:00
JonnyWong16
2803a6095b Add export for all collections and all playlists 2020-10-01 14:40:28 -07:00
JonnyWong16
d1172f4975 Add playlist method to plexapi.library.LibrarySection 2020-10-01 14:40:12 -07:00
JonnyWong16
478c4540b1 Add browsing photo info pages 2020-10-01 11:46:11 -07:00
JonnyWong16
cc1076e122 Add photo playlist items on info page 2020-10-01 10:38:32 -07:00
JonnyWong16
15e928ecf2 Add items suffix to collections and playlist table 2020-10-01 09:57:01 -07:00
JonnyWong16
2c360b6472 Fix searching in collections and playlist table 2020-09-30 22:41:28 -07:00
JonnyWong16
f5c99f712a Add get_collections_table and get_playlists_table to API docs 2020-09-30 22:10:06 -07:00
JonnyWong16
f151bb1451 Add datatables processing to collections and playlists tables 2020-09-30 22:09:19 -07:00
JonnyWong16
1061c334ae Improve look of collections and playlists tables 2020-09-30 21:01:14 -07:00
JonnyWong16
c5ea50d480 Improve playlist view on info page 2020-09-30 15:54:18 -07:00
JonnyWong16
84207effab Initial collections and playlists table to library page 2020-09-30 15:44:23 -07:00
JonnyWong16
b568af0a90 Add playlist info page 2020-09-30 14:00:57 -07:00
JonnyWong16
11f2f8ff81 Don't convert requiredBandwidths to list 2020-09-30 11:18:12 -07:00
JonnyWong16
454235dd9a Add XML export format 2020-09-30 00:04:27 -07:00
JonnyWong16
ad8dee3c47 Export locations attribute is media info 2020-09-29 23:21:32 -07:00
JonnyWong16
5dc0d5536d Also add username hover to most active card 2020-09-29 21:00:47 -07:00
JonnyWong16
86699ece8e Change custom fields to input instead of select 2020-09-29 20:22:16 -07:00
JonnyWong16
e3eca5af46 Change friendly name hover title text to username 2020-09-29 20:07:23 -07:00
JonnyWong16
e9f464e34d Add get_export_fields to API docs 2020-09-29 19:41:53 -07:00
JonnyWong16
97775e2a3b Add custom fields for collections and playlists 2020-09-29 19:40:53 -07:00
JonnyWong16
54433c43e6 Disable custom fields for selected export levels 2020-09-29 19:03:40 -07:00
JonnyWong16
3376908710 Add selectize disable-options plugin
* Modified to add a `disableField` option
2020-09-29 19:02:47 -07:00
JonnyWong16
284f77b9ae Add custom fields tree for shows and artists 2020-09-29 19:01:21 -07:00
JonnyWong16
2fe49f316f Fix typo in export table footer 2020-09-29 00:40:52 -07:00
JonnyWong16
1428a2485f Delay library.section.all() to speed up export return 2020-09-29 00:28:28 -07:00
JonnyWong16
3a1b4e34aa Don't show export table fetching message when auto-refreshing 2020-09-29 00:18:24 -07:00
JonnyWong16
26fb9a6803 Add option for custom fields to the export modal 2020-09-28 23:47:32 -07:00
JonnyWong16
31e6f4282d Move selectize.js to base.html 2020-09-28 20:48:00 -07:00
JonnyWong16
6bc7de7a6d Separate export poster and art 2020-09-28 20:47:28 -07:00
JonnyWong16
d9ece291b7 Fix 1px off dropdown menus 2020-09-28 18:23:07 -07:00
JonnyWong16
0203a1d4dc Use relative image path for export 2020-09-28 12:21:22 -07:00
JonnyWong16
221d6e136a Added remote access down notification threshold setting 2020-09-27 19:31:26 -07:00
JonnyWong16
ad6e314343 Don't floor newsletter start date 2020-09-27 17:44:11 -07:00
JonnyWong16
9fc4dbc6d6 Fix parsing custom fields for root media type 2020-09-27 17:17:38 -07:00
JonnyWong16
5dade92221 Add image export for collections and playlists 2020-09-27 17:11:08 -07:00
JonnyWong16
4e29960238 Save metadata and media info export level to database 2020-09-27 16:56:40 -07:00
JonnyWong16
3973c57020 Fix refresh export table button on info page 2020-09-27 16:55:50 -07:00
JonnyWong16
dd1dc00430 Rename API command to get_exports_table 2020-09-27 15:40:11 -07:00
JonnyWong16
9a7d6ea7d7 Hide rating key column on media export table 2020-09-27 15:39:39 -07:00
JonnyWong16
02d4a3b9fe Improve exporter API error messages 2020-09-27 15:30:10 -07:00
JonnyWong16
0a60d5f2b2 Add custom export level 0 2020-09-27 15:10:08 -07:00
JonnyWong16
13ff8f3a84 Refactor movie export levels 2020-09-27 14:39:22 -07:00
JonnyWong16
3efee000ce Add external guids to movie export 2020-09-27 14:32:10 -07:00
JonnyWong16
5915937975 Add collection labels to exporter 2020-09-27 14:28:09 -07:00
JonnyWong16
c7621a9e36 Add Guid tags to plexapi.video.Movie 2020-09-27 14:27:02 -07:00
JonnyWong16
28e2463c4f Add labels to plexapi.library.Collections 2020-09-27 14:26:32 -07:00
JonnyWong16
7016d3feea Remove extra export metadata button on collection page 2020-09-27 14:10:01 -07:00
JonnyWong16
44b4c10bf9 Update exporter API docs 2020-09-27 14:01:51 -07:00
JonnyWong16
be82c8f6d9 Add export table to info page 2020-09-27 13:59:11 -07:00
JonnyWong16
acebf96d2f Skip blank custom fields 2020-09-27 13:57:56 -07:00
JonnyWong16
27c5061d17 Add log message for processing custom fields 2020-09-27 13:30:10 -07:00
JonnyWong16
fcd034da00 Set export include_images if custom fields includes images 2020-09-27 13:27:37 -07:00
JonnyWong16
4ee9dbab41 Add ability to export custom fields 2020-09-27 13:26:04 -07:00
JonnyWong16
112811190e Fix exporting collections 2020-09-27 11:52:24 -07:00
JonnyWong16
746295aa16 Only export images for supported media types 2020-09-27 11:51:27 -07:00
JonnyWong16
693c0ba658 Refactor export get_any_hdr and get_image 2020-09-27 11:41:35 -07:00
JonnyWong16
e9f37d578e Refactor exporter 2020-09-27 11:27:16 -07:00
JonnyWong16
8f4da14611 Add collection export levels 2020-09-27 00:08:55 -07:00
JonnyWong16
063b7ce7cc Add note for export image media types 2020-09-26 23:09:18 -07:00
JonnyWong16
b7243271f3 Add photo album and photo export levels 2020-09-26 23:08:59 -07:00
JonnyWong16
395ab97191 Add titleSort to plexapi.photo.Photoalbum and plexapi.photo.Photo 2020-09-26 23:02:15 -07:00
JonnyWong16
76da200794 Add fields to plexapi.photo.Photoalbum and plexapi.photo.Photo 2020-09-26 22:53:39 -07:00
JonnyWong16
47695debdd Add artist, album, track export levels
* Refactor child levels
2020-09-26 22:48:47 -07:00
JonnyWong16
a5a2ba9d85 Export child images 2020-09-26 22:48:02 -07:00
JonnyWong16
3fa601db3e Add fields to plexapi.audio.Tracks 2020-09-26 22:40:37 -07:00
JonnyWong16
b60dcb2a23 Check path exists for adding exported image file size 2020-09-26 22:38:48 -07:00
JonnyWong16
1e173c6eeb Add working export sub media types 2020-09-26 21:42:51 -07:00
JonnyWong16
adb11db317 Use string format for export preview 2020-09-26 20:44:02 -07:00
JonnyWong16
068cb51635 Add zip file download for export with images 2020-09-26 20:36:51 -07:00
JonnyWong16
b1eab8bb0d Add include images to exporter 2020-09-26 19:26:24 -07:00
JonnyWong16
2a4b48d0fa Clean up Telegram send poster 2020-09-26 19:03:24 -07:00
JonnyWong16
a8e0502b41 Merge pull request #1377 from JohnnyKing94/master
Added Silent Notification option for Telegram Agent
2020-09-26 18:44:58 -07:00
JonnyWong16
ccf0e0dae7 Add default thumb and art to Live TV library 2020-09-26 18:32:13 -07:00
JonnyWong16
bfa4d3dfec Add library name to fix metadata modal 2020-09-26 18:03:29 -07:00
JonnyWong16
93997c11dc Add playback error notification trigger 2020-09-21 18:31:19 -07:00
JonnyWong16
7ce92d5f17 Add error state icon to activity card and history table 2020-09-21 18:30:41 -07:00
JonnyWong16
9740010368 Add container_decision to notification parameters 2020-09-21 18:06:40 -07:00
JonnyWong16
e4e0b765b6 Rename container transcoding to converting on activity cards 2020-09-21 17:57:01 -07:00
MichaIng
51d1dccb42 Avoid daemon forking with systemd
systemd units allow to run processes in foreground while daemonization is done on systemd service level when using Type=simple (default). This allows systemd to reliably track the service state, signals and could catch outputs, i.e. it is possible to remove "--quiet" to have Tautulli logging to systemd journal (journalctl) additionally or alternatively to log files.

In case of Type=forking, a PID file is required to allow system reliably determine the service state, which would be an alternative, but has no real advantage. The solution with "GuessMainPID=no" allows systemd to correctly determine the service active state, but e.g. when it is killed, it is seen as "Succeeded." since systemd cannot track the exit code or signal.
2020-09-21 23:58:30 +02:00
JonnyWong16
6f362ee2ad Rename export level options 2020-09-20 21:34:33 -07:00
JonnyWong16
f77bbda5ac Delete exported images folder 2020-09-20 21:31:48 -07:00
JonnyWong16
dceeaa77c5 Add checkbox to export images 2020-09-20 21:22:03 -07:00
JonnyWong16
d7c96d46e0 Refactor export get all metadata attrs 2020-09-20 21:21:37 -07:00
JonnyWong16
b9f5251188 Export art and thumb images 2020-09-20 21:04:57 -07:00
JonnyWong16
75cdc2c5e8 Add art and thumb url to plexapi collections 2020-09-20 21:03:05 -07:00
JonnyWong16
7eedb14834 Refactor exporter into Export class 2020-09-20 20:34:31 -07:00
JonnyWong16
ca06154805 Separate metadata and media export levels 2020-09-20 13:02:02 -07:00
JonnyWong16
35cdef1340 Add hdr attribute to media export 2020-09-20 12:27:42 -07:00
JonnyWong16
d609c0daeb Add export metadata button to collections 2020-09-20 11:23:19 -07:00
JonnyWong16
db0b157d43 Add admin auth to export modal 2020-09-20 11:23:19 -07:00
JonnyWong16
906aedd2f1 Add movie export levels 2020-09-20 11:23:19 -07:00
JonnyWong16
07a9bdbde3 Add more stream export attributes 2020-09-20 11:23:19 -07:00
JonnyWong16
588b1b1bc3 Add more stream attributes to plexapi 2020-09-20 11:23:19 -07:00
JonnyWong16
eb63f89b1f Add helper functions for export levels 2020-09-20 11:23:19 -07:00
JonnyWong16
fb81d1b6f3 Add human duration and file size to export attributes 2020-09-20 11:23:19 -07:00
JonnyWong16
b897212050 Update human_file_size helper function 2020-09-20 11:23:19 -07:00
JonnyWong16
3f6612fe9a Update human_duration helper function 2020-09-20 11:23:19 -07:00
JonnyWong16
bf1a59c5c0 Remove traceback for export 2020-09-20 11:23:19 -07:00
JonnyWong16
6fb3a3a3c8 Update export json and csv for Python 2 2020-09-20 11:23:19 -07:00
JonnyWong16
ed454b2a4a Add backports.csv 1.0.7 2020-09-20 11:23:19 -07:00
JonnyWong16
27f828e619 Message to view export on library page 2020-09-20 11:23:19 -07:00
JonnyWong16
bde0ce20d8 Fix export modal file format not working on library page 2020-09-20 11:23:19 -07:00
JonnyWong16
28c6163a31 Exporter check rating key first 2020-09-20 11:23:19 -07:00
JonnyWong16
14bb377794 Add link to export filename to view in browser 2020-09-20 11:23:19 -07:00
JonnyWong16
61c692ad4e Cancel processing exports on startup 2020-09-20 11:23:19 -07:00
JonnyWong16
42856e5ac8 Fix export modal selected file format 2020-09-20 11:23:19 -07:00
JonnyWong16
e82ad09a8d Rename row_id to exporr_id 2020-09-20 11:23:19 -07:00
JonnyWong16
40fbc55ab3 Add file size to export table 2020-09-20 11:23:19 -07:00
JonnyWong16
a27a5b023b Add file format to export modal 2020-09-20 11:23:19 -07:00
JonnyWong16
a7eb563c2e Move export thread so table can be refreshed 2020-09-20 11:23:19 -07:00
JonnyWong16
43fefcf748 Add export modal and buttons to library and info page 2020-09-20 11:23:19 -07:00
JonnyWong16
621fb95227 Auto-refresh export table if an item is processing 2020-09-20 11:23:19 -07:00
JonnyWong16
d3704fcee6 Add ability for custom calculated attributes for exporting 2020-09-20 11:23:19 -07:00
JonnyWong16
55100dfb7a Fix select columns button not showing for export table 2020-09-20 11:23:19 -07:00
JonnyWong16
010fefcbbc Link rating key in export table to info page 2020-09-20 11:23:19 -07:00
JonnyWong16
7627f025ed Change button colours on export table 2020-09-20 11:23:19 -07:00
JonnyWong16
e256d2080d Make export work on Python 2 and set failed state 2020-09-20 11:23:19 -07:00
JonnyWong16
58292067f0 Fix export csv missing dict keys 2020-09-20 11:23:19 -07:00
JonnyWong16
d5e91801d6 Fix downloading export file 2020-09-20 11:23:19 -07:00
JonnyWong16
91d545f480 Disable export delete button if still processing 2020-09-20 11:23:19 -07:00
JonnyWong16
9c2599acbe No export section_id for playlists 2020-09-20 11:23:19 -07:00
JonnyWong16
06341ee632 Fix export doc strings 2020-09-20 11:23:19 -07:00
JonnyWong16
bcc693e4c7 Add error message for failed export download 2020-09-20 11:23:19 -07:00
JonnyWong16
8b8afacaea Add function to delete exported files 2020-09-20 11:23:19 -07:00
JonnyWong16
deb49d7ff9 Add function to download exported files 2020-09-20 11:23:19 -07:00
JonnyWong16
6334ffa197 Add export directory 2020-09-20 11:23:19 -07:00
JonnyWong16
b872cce2a4 Check exported file exists 2020-09-20 11:23:19 -07:00
JonnyWong16
de2e2ee962 Add file_format to exports table 2020-09-20 11:23:19 -07:00
JonnyWong16
5468676811 Add table to list exported items 2020-09-20 11:23:19 -07:00
JonnyWong16
c102020698 Add metadata export function 2020-09-20 11:23:19 -07:00
JonnyWong16
0ff363b6ee Add export helper functions 2020-09-20 11:23:19 -07:00
JonnyWong16
c324cf69ed Merge custom plexapi 3.6.0-tautulli 2020-09-20 11:23:19 -07:00
Gianfranco
721cf5c930 Renamed 'silent_message' to 'silent_notification.'
Signed-off-by: Gianfranco <gianfry94@hotmail.it>
2020-09-14 20:46:22 +02:00
Gianfranco
f07bcca96a Wording changes
Signed-off-by: Gianfranco <gianfry94@hotmail.it>
2020-09-14 20:26:27 +02:00
JonnyWong16
056d0d81ac Improve activity monitor session start log message 2020-09-14 09:19:33 -07:00
JonnyWong16
38ccd37867 Fix QR code not showing up for localhost address 2020-09-14 08:51:22 -07:00
Gianfranco
21799116c5 Reworked the Telegram Agent code to include the "silent_message" option. Both cases are now managed and the alerts are being respected.
Signed-off-by: Gianfranco <gianfry94@hotmail.it>
2020-09-14 12:46:45 +02:00
JonnyWong16
60bdf1d1ce Schedule database pragma optimize 2020-09-12 14:18:31 -07:00
JonnyWong16
02658759ea Fix purge library from the edit modal 2020-09-10 08:35:26 -07:00
JonnyWong16
68946ceede Add uninstall before installing to Windows installer 2020-09-06 19:03:47 -07:00
JonnyWong16
9184ae4608 v2.5.5 2020-09-06 14:43:16 -07:00
JonnyWong16
de64b5ddfa Revert "Add negative margin to sections with fixed cards"
This reverts commit 668c9e6045.

* Revert #1378
2020-09-06 14:18:25 -07:00
JonnyWong16
b3ffbbf3ea Patch osxnotify for Python 3 compatibility
* Fixes Tautulli/Tautulli-Issues#276
2020-09-06 14:14:08 -07:00
JonnyWong16
aa80fdf738 Merge pull request #1380 from mvanbaak/issue_277_update_ipaddr
Upgrade ipaddr from 2.1.11 to 2.2.0. Its now python 3 compatible

* Fixes Tautulli/Tautulli-Issues#277
2020-09-06 13:59:13 -07:00
JonnyWong16
9ad95f51d4 Fix whois lookup failing in some instances 2020-09-06 13:57:42 -07:00
JonnyWong16
0902a61341 Update profilehooks to 1.12.0
* Fixes Tautulli/Tautulli-Issues#275
2020-09-06 13:45:01 -07:00
JonnyWong16
55ffd54e5b Filter out background theme music sessions 2020-09-04 18:29:37 -07:00
JonnyWong16
e014bfa63e Log selected Plex server 2020-09-04 18:27:28 -07:00
JonnyWong16
687672e9c1 Fix plural seasons in recently added 2020-09-04 08:01:51 -07:00
Michiel van Baak
137889dc9c Upgrade ipaddr from 2.1.11 to 2.2.0. Its now python 3 compatible 2020-09-03 16:46:37 +02:00
JonnyWong16
f24f4a4250 Fix checking pid file on startup 2020-08-29 12:35:04 -07:00
JonnyWong16
95fc108d57 Merge pull request #1378 from dotsam/margin-fix
Add negative margin to sections with fixed cards
2020-08-29 12:31:58 -07:00
JonnyWong16
95f48ba9f6 Return empty results for API instead of null error
* Fixes Tautulli/Tautulli-Issues#274
2020-08-26 18:56:49 -07:00
JonnyWong16
d80cf232c8 Add multi-column sorting to datatables API commands 2020-08-26 18:32:06 -07:00
JonnyWong16
ab3ec875a3 Return custom library art for datatable 2020-08-26 17:53:50 -07:00
Sam Edwards
668c9e6045 Add negative margin to sections with fixed cards 2020-08-23 23:14:45 -07:00
JonnyWong16
67b452a461 Fix user and library recently played sorting order 2020-08-23 18:17:41 -07:00
Gianfranco
81ee44b60f Added "silent_message" option for Telegram Agent
Added a new checkbox in the notification telegram config in order to send new messages silently. In this way the telegram users will receive a notification with no sound.
2020-08-21 22:49:26 +02:00
JonnyWong16
9b3bfd14db Check external guids for notification parameters 2020-08-21 11:58:07 -07:00
JonnyWong16
e00c8fb186 Add external guids to metadata 2020-08-21 11:57:43 -07:00
JonnyWong16
a0919e246d Use pyinstaller==3.6 for package builds 2020-08-21 11:10:14 -07:00
JonnyWong16
003f684f8a Override thumb for clips using the art rating_key 2020-08-21 10:53:05 -07:00
JonnyWong16
69d55c60c3 Add icon and thumb for clips on history table 2020-08-21 10:53:05 -07:00
JonnyWong16
560094dcf6 Add logger message for generating newsletter 2020-08-21 10:53:05 -07:00
JonnyWong16
4edd6ce911 Add scheduled task to optimize database 2020-08-21 10:52:26 -07:00
JonnyWong16
f76bd2af8e Merge pull request #1376 from nwithan8/patch-1
Spelling error
2020-08-11 14:32:21 -07:00
Nate Harris
7747503fee Spelling error 2020-08-09 22:40:47 -04:00
JonnyWong16
1e1a8ddfb0 Fix get_logs API command encoding error
* Fixes Tautulli/Tautulli-Issues#269
2020-08-08 21:37:09 -07:00
JonnyWong16
9bcd18f1b6 Remove revealed characters in masked log 2020-08-08 21:31:50 -07:00
JonnyWong16
50b6f9a8f2 Blacklist password parameter in get_apikey command 2020-08-08 21:26:01 -07:00
JonnyWong16
b4ba88b3e5 Fix get_apikey API command with a hashed password 2020-08-08 21:19:39 -07:00
JonnyWong16
ba9acd6e23 Add auth to some admin endpoints 2020-08-05 21:21:19 -07:00
JonnyWong16
dd9513313b Don't highlight links in scheduler table 2020-08-05 20:56:44 -07:00
JonnyWong16
288a1c86ab Replace white with "not white" 2020-08-05 20:54:12 -07:00
JonnyWong16
8e28cb10fa Rename terminate session to terminate stream 2020-08-05 09:02:18 -07:00
JonnyWong16
3d35a525d3 Make sure json response is encoded to utf-8 2020-08-03 21:45:11 -07:00
JonnyWong16
f7153d0f3b Fix Local user icon not showing in library user stats 2020-08-03 11:45:43 -07:00
JonnyWong16
4285b55c15 Update timestamp helper functions 2020-08-03 10:29:45 -07:00
JonnyWong16
b54576f08f Fix download API commands not returning the file
* Fixes Tautulli/Tautulli-Issues#268
2020-08-02 22:09:40 -07:00
JonnyWong16
6b4db681ff Fix get_synced_items API command returning error with empty result
* Fixes Tautulli/Tautulli-Issues#267
2020-08-02 22:03:35 -07:00
JonnyWong16
f582f781f3 Update helpers.now function 2020-08-02 13:48:10 -07:00
JonnyWong16
9baecb0a41 Change webstart failure error message 2020-08-02 10:29:57 -07:00
JonnyWong16
91a18e1a92 Add get_server_info API command 2020-08-02 10:18:57 -07:00
JonnyWong16
acfbb0e96d Add import_config to API docs 2020-08-02 10:18:25 -07:00
JonnyWong16
c52292962d Remove mock 2020-07-31 22:06:23 -07:00
JonnyWong16
6e53743716 Update plexapi to v3.6.0 2020-07-31 22:06:07 -07:00
JonnyWong16
873194b402 Add hidden import pkg_resources.py2_warn to Windows installer 2020-07-31 15:14:31 -07:00
JonnyWong16
21dec5feb3 v2.5.4 2020-07-31 14:24:08 -07:00
JonnyWong16
bee4106af0 Change direct stream icon 2020-07-31 14:23:01 -07:00
JonnyWong16
bbb6e46515 Replace sys.stderr with logger 2020-07-27 18:47:08 -07:00
JonnyWong16
570ebb4f73 Add plex_id to notification parameters 2020-07-27 18:38:04 -07:00
JonnyWong16
d93204af4e Lookup TVmaze using title 2020-07-27 18:30:00 -07:00
JonnyWong16
702f116db9 Lookup The Movie Database using title and year 2020-07-27 18:20:12 -07:00
JonnyWong16
0c8607b3ec Fix typo in QR modal 2020-07-25 13:28:39 -07:00
JonnyWong16
3a2cc6efc7 Trim address when generating the QR code 2020-07-25 13:28:39 -07:00
JonnyWong16
1b37ff1655 Mobile device registration temporarily assume device_id is onesignal_id 2020-07-25 13:28:34 -07:00
JonnyWong16
769934c8a5 Add server_id to Andoird App notification data 2020-07-25 13:21:52 -07:00
JonnyWong16
7f1a4ec34a Add return PMS name and server ID from device registration 2020-07-25 13:21:52 -07:00
JonnyWong16
27438f7915 Don't allow apikey when using an app 2020-07-25 13:21:51 -07:00
JonnyWong16
8651bef9c1 Mask onesignal_id from API logs 2020-07-25 13:21:51 -07:00
JonnyWong16
36324d10dc Add onesignal_id to register device API 2020-07-25 13:21:46 -07:00
JonnyWong16
0272c35047 Fix parsing request responst message 2020-07-25 11:59:24 -07:00
JonnyWong16
70c0f912e2 Add themoviedb rating image 2020-07-24 09:12:11 -07:00
JonnyWong16
59a6acc088 Fix encoding issue with websocket logging 2020-07-23 17:44:17 -07:00
JonnyWong16
10b0726727 Remove support for .exe from script notifications 2020-07-23 17:33:43 -07:00
JonnyWong16
8f1360d7c2 Check for valid script extension when using a prefix override
* Also removes php, ruby, and perl overrides
2020-07-23 17:33:36 -07:00
JonnyWong16
e0e5ac9ecc Check for a valid script and script extension 2020-07-22 18:55:14 -07:00
JonnyWong16
c814f219a2 Prevent simultaneous importing of database/config 2020-07-22 18:33:47 -07:00
JonnyWong16
9095fc0c7a Catch config.ParseError 2020-07-22 18:27:23 -07:00
JonnyWong16
a675202537 Browse path starting from from current value 2020-07-18 15:19:42 -07:00
JonnyWong16
b52ab4885b Add browser button for script folder 2020-07-18 12:13:42 -07:00
JonnyWong16
43e26c9b56 Add Plex logs folder to config not imported note 2020-07-16 19:38:20 -07:00
JonnyWong16
703a7feed2 Update help text for SSL certificates/key in PEM format 2020-07-16 19:29:11 -07:00
JonnyWong16
7b69ed4cec Add browse function to settings with a folder or file 2020-07-16 19:27:14 -07:00
JonnyWong16
fcca7f969e Add filter extension as data property 2020-07-16 19:26:24 -07:00
JonnyWong16
ec34ea2116 Trigger change and unbind after selecting in file browser 2020-07-16 19:05:18 -07:00
JonnyWong16
3dc36c3b92 Refactor browse path function 2020-07-16 18:19:43 -07:00
JonnyWong16
f0d4fd5523 Add placeholder text for database/config import 2020-07-16 18:19:20 -07:00
JonnyWong16
7fe6c72fe2 Do not import PMS logs folder from config 2020-07-16 18:01:47 -07:00
JonnyWong16
d216d0f27f Reword import help text 2020-07-16 00:02:13 -07:00
JonnyWong16
43a7758acd Cleanup database import modal 2020-07-15 23:53:01 -07:00
JonnyWong16
3043956dec Add config import to settings page 2020-07-15 23:51:48 -07:00
JonnyWong16
06665fdd06 Add fucntion to import a config file 2020-07-15 23:26:22 -07:00
JonnyWong16
beff5caaac Clean shutdown page 2020-07-15 22:53:46 -07:00
JonnyWong16
3859412b2c Fix database import API docs 2020-07-15 22:10:17 -07:00
JonnyWong16
f7ec476fc0 Remove more unused config keys 2020-07-15 21:25:34 -07:00
JonnyWong16
b97d32671d Remove unused library update functions 2020-07-15 21:21:17 -07:00
JonnyWong16
01c56ef280 Remove helper bool check in database import status 2020-07-15 21:06:28 -07:00
JonnyWong16
b9422312f3 Remove unused check recently added pinger 2020-07-15 21:05:49 -07:00
JonnyWong16
9a0f83c3e7 Remove old config updates 2020-07-15 21:04:30 -07:00
JonnyWong16
fbfedb2e62 Remove unused config keys 2020-07-15 21:04:22 -07:00
JonnyWong16
4f8a462041 Update chown instructions in systemd script 2020-07-13 19:08:20 -07:00
JonnyWong16
141d043a6a FreeBSD/FreeNAS python is python3 2020-07-13 19:08:19 -07:00
JonnyWong16
c1266fed12 Update API docs for database import 2020-07-13 19:08:14 -07:00
JonnyWong16
4a4be9798d Adjust user IP table column widths 2020-07-12 12:54:56 -07:00
JonnyWong16
172692ccca Fix user IP table showing first played instead of last played 2020-07-12 12:54:42 -07:00
JonnyWong16
50e7c0469f Merge pull request #1374 from dotsam/ip-first-streamed
Add first_seen to user IP Table
2020-07-12 12:41:46 -07:00
JonnyWong16
44f74e3590 Mask device token and device id from API logs 2020-07-12 10:35:52 -07:00
Sam Edwards
63656b73c2 Add first_seen to user ips and add title attr with full date/time 2020-07-11 15:23:26 -07:00
JonnyWong16
40ecf56904 Fix Cloudinary upload for non-ASCII characters on Python 2 2020-07-10 21:57:53 -07:00
JonnyWong16
b4a10adec2 Merge branch 'v2.5-monitor-remote-access' into nightly 2020-07-10 17:09:58 -07:00
JonnyWong16
1698622d63 v2.5.3 2020-07-10 17:07:18 -07:00
JonnyWong16
fa27271647 Change shebang on contrib scripts 2020-07-10 17:02:23 -07:00
JonnyWong16
d837811c68 Improve start script 2020-07-09 17:13:16 -07:00
JonnyWong16
ad195f0969 Fix deleteing more than 1000 history entries at the same time 2020-07-08 12:27:20 -07:00
JonnyWong16
4a8748e322 Live TV library not being recreated after server identifier is changed
* Fixes Tautulli/Tautulli-Issues#261
2020-07-07 18:14:00 -07:00
JonnyWong16
0f016c83ea Fix ipwhois data location for macOS package
* Fixes Tautulli/Tautulli-Issues#260
2020-07-07 17:25:46 -07:00
JonnyWong16
061ae44da4 Fix indentation in macOS postinstall script 2020-07-07 17:05:15 -07:00
JonnyWong16
a8b90bf100 Reduce macOS build requirement to pyobjc-framework-Cocoa 2020-07-07 17:05:10 -07:00
JonnyWong16
eb3cd49bc4 Add hidden import pkg_resources.py2_warn to macos.spec
* Fixes build on macOS 10.13 (High Sierra)
2020-07-06 20:57:37 -07:00
JonnyWong16
416d869288 Add python version to Google Analytics 2020-07-06 18:13:33 -07:00
JonnyWong16
a116c26c25 Run python scripts with the same sys.executable as Tautulli 2020-07-06 11:32:16 -07:00
JonnyWong16
cc4ec53dac Full path to python3 interpreter in FreeBSD startup script 2020-07-06 10:08:36 -07:00
JonnyWong16
63164c7ff5 Quote command in systemd script 2020-07-06 09:37:37 -07:00
JonnyWong16
9815c014e8 Add python interpreter to init-scripts 2020-07-06 09:30:35 -07:00
JonnyWong16
69675151bf Remove monitor remote access settings
* Tautulli/Tautulli-Issues#251
2020-07-05 20:40:44 -07:00
JonnyWong16
99e395ddfa Update scheduled tasks table
* Tautulli/Tautulli-Issues#251
2020-07-05 20:39:31 -07:00
JonnyWong16
7fe1e542df Remove check remote access scheduled task
* Tautulli/Tautulli-Issues#251
2020-07-05 20:38:54 -07:00
JonnyWong16
938134081b Add remote access monitoring using websockets
* Fixes Tautulli/Tautulli-Issues#251
2020-07-05 20:36:44 -07:00
JonnyWong16
3fd2234a92 Remove refresh reachability 2020-07-05 19:20:52 -07:00
JonnyWong16
41843dc573 Rename some column headers 2020-07-04 12:22:40 -07:00
JonnyWong16
cc6bd528a5 Add architecture to release assets 2020-07-04 11:28:22 -07:00
JonnyWong16
2625ef5fb9 Use Popen to restart on macOS 2020-07-03 19:48:27 -07:00
JonnyWong16
dbd2d28877 Set macOS menu bar icon thread to daemon 2020-07-03 19:47:57 -07:00
JonnyWong16
f70f814c70 Shutdown tray icons last 2020-07-03 19:47:11 -07:00
JonnyWong16
6710e42134 Hide macOS dock icon for pkg install 2020-07-03 19:46:27 -07:00
JonnyWong16
78c5b45e43 Also fix e562ec9 for Python 2 2020-07-03 11:24:47 -07:00
JonnyWong16
e562ec96fa Fix encoding when reading a newsletter file 2020-07-02 20:46:42 -07:00
JonnyWong16
9b5e01c319 Fix logger for email notification exception 2020-07-02 12:45:45 -07:00
JonnyWong16
0097532f4a Fix startup script 2020-07-02 12:33:32 -07:00
JonnyWong16
91935c9018 Add hidden import cheroot.ssl.builtin for pyinstaller 2020-07-02 09:20:58 -07:00
JonnyWong16
83df807f7e Fix typo in eb3db20 2020-07-02 09:15:13 -07:00
JonnyWong16
eb3db20340 Add hidden import chroot.ssl for pyinstaller 2020-07-02 09:11:15 -07:00
JonnyWong16
6dab6194ea Replace which with command -v in startup script 2020-07-01 22:44:05 -07:00
JonnyWong16
356f64cac0 v2.5.2 2020-07-01 19:49:44 -07:00
JonnyWong16
f77f289125 Move GitHub sponsor first 2020-07-01 15:53:08 -07:00
JonnyWong16
280257477a Revert "Change shebang to python3"
This reverts commit cd8a899521.
2020-07-01 14:59:18 -07:00
JonnyWong16
660141cb16 Try various python versions in startup script 2020-07-01 14:43:35 -07:00
JonnyWong16
cd8a899521 Change shebang to python3 2020-07-01 14:43:04 -07:00
1796 changed files with 13032 additions and 308530 deletions

View File

@@ -1,10 +0,0 @@
.git
.github
.gitignore
contrib
init-scripts
package
pylintrc
*.md
!CHANGELOG*.md
start.bat

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: JonnyWong16
patreon: Tautulli
custom: ["https://bit.ly/2InPp15"]

View File

@@ -1,95 +0,0 @@
name: Publish Docker
on:
push:
branches: [master, beta, nightly, python3]
tags: [v*]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Prepare
id: prepare
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
elif [[ $GITHUB_REF == refs/heads/master ]]; then
echo ::set-output name=tag::latest
else
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
fi
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo ::set-output name=branch::master
else
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
fi
echo ::set-output name=commit::${GITHUB_SHA}
echo ::set-output name=build_date::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64,linux/arm
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v3
with:
buildx-version: latest
- name: Cache Docker Layers
id: cache
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker Buildx (no push)
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform ${{ steps.prepare.outputs.docker_platforms }} \
--output "type=image,push=false" \
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
--file Dockerfile .
- name: Docker Login
if: success()
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Docker Buildx (push)
if: success()
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--platform ${{ steps.prepare.outputs.docker_platforms }} \
--output "type=image,push=true" \
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
--file Dockerfile .
- name: Clear
if: always()
run: |
rm -f ${HOME}/.docker/config.json
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: ${{ github.workflow }}
nofail: true

View File

@@ -1,204 +0,0 @@
name: Publish Release
on:
push:
branches: [master, beta, nightly, python3]
tags: [v*]
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set Release Version
id: get_version
shell: bash
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION_NSIS=${GITHUB_REF#refs/tags/v}.1
echo ::set-output name=VERSION_NSIS::${VERSION_NSIS/%-beta.1/.0}
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
else
echo ::set-output name=VERSION_NSIS::0.0.0.0
echo ::set-output name=VERSION::0.0.0
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
fi
echo $GITHUB_SHA > version.txt
- name: Set Up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Cache Dependencies
id: cache_dependencies
uses: actions/cache@v2
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
restore-keys: ${{ runner.os }}-pip-
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r package/requirements-windows.txt
- name: Build Package
run: |
pyinstaller -y ./package/Tautulli-windows.spec
- name: Create Installer
uses: joncloud/makensis-action@v1.2
with:
script-file: ./package/Tautulli.nsi
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
include-more-plugins: true
include-custom-plugins-path: package/nsis-plugins
- name: Upload Installer
uses: actions/upload-artifact@v2
with:
name: Tautulli-windows-installer
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: Build Windows Installer
nofail: true
build-macos:
runs-on: macos-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set Release Version
id: get_version
shell: bash
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo ::set-env name=VERSION::${GITHUB_REF#refs/tags/v}
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
else
echo ::set-env name=VERSION::0.0.0
echo ::set-output name=VERSION::0.0.0
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
fi
echo $GITHUB_SHA > version.txt
- name: Set Up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Cache Dependencies
id: cache_dependencies
uses: actions/cache@v2
with:
path: ~/Library/Caches/pip
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
restore-keys: ${{ runner.os }}-pip-
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r package/requirements-macos.txt
- name: Build Package
run: |
pyinstaller -y ./package/Tautulli-macos.spec
- name: Create Installer
run: |
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
- name: Upload Installer
uses: actions/upload-artifact@v2
with:
name: Tautulli-macos-installer
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: Build MacOS Installer
nofail: true
release:
needs: [build-windows, build-macos]
if: startsWith(github.ref, 'refs/tags/') && always()
runs-on: ubuntu-latest
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v1
- name: Checkout Code
uses: actions/checkout@v2
- name: Set Release Version
id: get_version
run: |
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
- name: Download Windows Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/download-artifact@v2
with:
name: Tautulli-windows-installer
- name: Download MacOS Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/download-artifact@v2
with:
name: Tautulli-macos-installer
- name: Get Changelog
id: get_changelog
run: echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_version.outputs.RELEASE_VERSION }}
release_name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }}
body: |
## Changelog
##${{ steps.get_changelog.outputs.CHANGELOG }}
draft: false
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
- name: Upload Windows Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
asset_content_type: application/vnd.microsoft.portable-executable
- name: Upload MacOS Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
asset_content_type: application/vnd.apple.installer+xml

340
.gitignore vendored
View File

@@ -1,82 +1,280 @@
# Compiled source #
###################
*.pyc
*.py~
*.pyproj
*.sln
# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python,linux,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python,linux,windows
# PlexPy files #
######################
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
*.pot
# Django stuff:
*.log
*.db*
*.db-journal
*.ini
release.lock
version.lock
logs/*
backups/*
cache/*
newsletters/*
*.mmdb
version.txt
branch.txt
local_settings.py
db.sqlite3
db.sqlite3-journal
# HTTPS Cert/Key #
##################
/*.crt
/*.key
/*.csr
/*.pem
# Flask stuff:
instance/
.webassets-cache
# Mergetool
*.orgin
# Scrapy stuff:
.scrapy
# OS generated files #
######################
.DS_Store?
.DS_Store
ehthumbs.db
Icon?
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
#Ignore files generated by PyCharm
*.idea/*
# Dump file
*.stackdump
#Ignore files generated by vi
*.swp
# Folder config file
[Dd]esktop.ini
#Ignore files build by Visual Studio
*.obj
*.exe
*.pdb
*.user
*.aps
*.pch
*.vspscc
*_i.c
*_p.c
*.ncb
*.suo
*.tlb
*.tlh
*.bak
*.cache
*.ilk
*.log
[Bb]in
[Dd]ebug*/
*.lib
*.sbr
obj/
[Rr]elease*/
_ReSharper*/
[Tt]est[Rr]esult*
/cache
/logs
.project
.pydevproject
# Recycle Bin used on file shares
$RECYCLE.BIN/
#Ignore files generated by pyinstaller
/build
/dist
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python,linux,windows
/config.ini
/release.lock
/tautulli.db
/version.lock
/cache/

2895
API.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,195 @@
# Changelog
## v2.5.2-beta (2020-06-27)
## v2.6.5 (2021-01-09)
* Other:
* Fix: Login to Tautulli not working on Python 2.
* Fix: Some IP addresses not being masked in the logs.
* New: Auto-updater for Windows exe installer.
* Change: Allow Snap package to access the user home directory.
* Change: Migrate Snap user data to a persistent location that is retained if Tautulli is reinstalled.
## v2.5.1-beta (2020-06-26)
## v2.6.4 (2020-12-20)
* Other:
* Fix: Restore Snap data folder from previous installs.
## v2.6.3 (2020-12-19)
* Announcements:
* This is the last Tautulli version to support Python 2. Python 3 will be required to continue receiving updates. You can check your Python version on the settings page.
* Exporter:
* Fix: Accessible and exists attributes were blank for media info export level 9.
* UI:
* Fix: Guest usernames were not masked on mouse hover.
* Other:
* Fix: macOS menu bar icon for light and dark mode.
* New: Tautulli can officially be installed on Linux using a Snap package. See the installation wiki for details.
## v2.6.2 (2020-12-05)
* Notifications:
* Change: Send a notification of a user new device for the first time only. This can be toggled off in the settings.
* Exporter:
* Fix: Allow exporting child fields only without requiring the parent fields as well.
* Fix: Exporting individual collection would fail.
* Change: Remove accessible and exists fields from the default media info export levels. This prevents the Plex server from reading the media files unnecessarily.
* Other:
* Fix: Enable high resolution for the macOS system tray icon and menu.
* New: Added rate limiting for failed login attempts.
* Change: Use a white logo for the macOS system tray icon.
* API:
* New: Added machine_id to the get_history API response.
## v2.6.1 (2020-11-03)
* Other:
* Fix: High CPU/memory usage in some instances.
* Fix: Logger error preventing Tautulli from starting.
* Fix: Database issue with non-unique image hashes.
## v2.6.0 (2020-10-31)
* Exporter:
* New: New exporter feature that allows you to export the metadata and images for any library, collection, playlist, or media item to csv, json, xml, or m3u8. Refer to the Exporter Guide in the wiki for more details.
* UI:
* Fix: Margin on the homepage activity and statistic/library cards. (Thanks @dotsam)
* Fix: Movie ratings not showing on the info page for the new Plex Movie agent.
* New: Added ability to browse collections and playlists from the library and user pages.
* Change: Updated platform brand logos and colours.
* API:
* New: Added export_metadata, download_export, and delete_export API commands.
* New: Added get_collections_table, and get_playlists_table API commands.
* New: Added min_version parameter to the register_device API command.
* New: Added include_activity parameter to the get_history API command.
* New: Added sync_id parameter to the get_metadata API command.
* New: Added delete_synced_item API command.
* New: Added a stat_id and stats_start parameters to the get_home_stats API command.
* New: Allow deleting a mobile device using the registration device_id for the delete_mobile_device API command.
* Change: Return Plex server info and Tautulli info from the register_device command.
* Other:
* New: The Docker container is now also built for the arm32v6 architecture.
* New: The Docker container is also published to the GitHub Container Registry at ghcr.io/tautulli/tautulli.
* Change: Tautulli is now using a forked version of plexapi 3.6.0. This is to support the exporter feature while still maintaining Python 2 compatibility.
* Change: Updated systemd script to remove process forking. (Thanks @MichaIng)
* Change: Cache GitHub update check on startup.
## v2.5.6 (2020-10-02)
* Activity:
* Change: Renamed container "Transcode" to "Converting" on activity cards.
* Notifications:
* New: Added a silent notification option for Telegram. (Thanks @JohnnyKing94)
* New: Added container_decision notification parameter.
* New: Added notification trigger for Playback Error.
* New: Added remote access down notification threshold setting.
* Newsletter:
* Change: Stop flooring newsletter start date.
* UI:
* Fix: Unable to purge history from the library edit modal.
* Fix: QR code not showing up for localhost address when trying to register a device.
* New: Added library name to the fix metadata modal.
* API:
* New: Added default thumb and art to the Live TV library.
* Other:
* Fix: Synced items not loading for guest access.
* New: Schedule some more automatic database optimizations.
* Change: Added automatic uninstall before installing to the Windows installer.
## v2.5.5 (2020-09-06)
* Activity:
* Fix: Filter out TV show background theme music sessions.
* Notifications:
* New: Check Plex external guids for notification metadata provider links.
* UI:
* Fix: Incorrect sorting for user/library recently played items.
* API:
* Fix: get_synced_items API command returning error with empty result.
* Fix: Download API commands not returning the file.
* Fix: get_logs API command encoding error.
* Fix: get_user_player_stats API command returning error instead of empty result.
* New: Added get_server_info API command.
* New: Added external guids to get_metadata API command.
* New: Added support for multi-column sorting for datatable API commands.
* Change: get_activity API command return thumbnail override for clips.
* Change: get_libraries_table API command return custom library artwork.
* Other:
* Fix: Tautulli failed to run with a stale pid file.
* New: Added scheduled task to optimize the Tautulli database.
* Change: Update plexapi to 3.6.0.
* Change: Update some libraries for Python 3 compatibility.
## v2.5.4 (2020-07-31)
* Monitoring:
* Change: Montitoring remote access changed to use websockets. Refer to Tautulli/Tautulli-Issues#251 for details.
* Notifications:
* Fix: Uploading images to Cloudinary failed for titles with non-ASCII characters on Python 2.
* New: Added plex_id notification parameter.
* Remove: Running .exe files directly using script notifications is no longer supported.
* Remove: php, perl, and ruby prefix overrides for script notifications is no longer supported.
* Change: Stricter checking of file extensions for script notifications.
* Change: Fallback to The Movie Database lookup using title and year.
* Change: Fallback to TVmaze lookup using title.
* UI:
* New: Added ability to import a previous Tautullli configuration file in the settings.
* New: Added a browse button for settings which require a folder or file input.
* New: Added first streamed column to user IP addresses table. (Thanks @dotsam)
* New: Added The Movie Database rating image to media page.
* Change: Different icon to represent direct stream in the history tables.
* API:
* New: Updated API docs for importing a database and configuration file.
## v2.5.3 (2020-07-10)
* History:
* Fix: Unable to delete more than 1000 history entries at the same time.
* Notifications:
* Change: Python script notifications to run using the same Python interpreter as Tautulli.
* Newsletters:
* Fix: Unable to view newsletters with special characters.
* Other:
* Fix: Tautulli failing to start after enabling HTTPS when installed using the Windows / macOS installers.
* Fix: Startup script not working on macOS.
* Fix: Unable to hide dock icon on macOS with the pkg install. Refer to the FAQ regarding the Python rocket dock icon.
* Change: Added path to Python interpreter in system startup (daemon) scripts.
* Change: Added Python version to Google analytics.
## v2.5.2 (2020-07-01)
* Announcements:
* Tautulli now supports Python 3!
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
* Notifications:
* Fix: Error uploading images to Cloudinary on Python 2.
* Fix: Testing browser notifications alert not disappearing.
* Change: Default recently added notification delay set to 300 seconds.
* UI:
* Fix: MacOS menu bar icon causing Tautulli to fail to start.
* New: Added platform icon for LG devices.
* Mobile App:
* Fix: Improved API security and validation when registering the Android app.
* Other:
* Fix: Error creating self-signed certificates on Python 3.
* Fix: Docker container not respecting the PUID and PGID environment variables.
* Fix: Tautulli login session cookie not set on the HTTP root path.
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
## v2.5.0-beta (2020-05-31)
* Announcements:
* Tautulli now supports Python 3!
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
* UI:
* Fix: Unable to login to Tautulli on Python 2.
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
* New: Added menu bar icon for MacOS.
* New: Ability to import a Tautulli database in the settings.
* New: Added Tautulli news area on the settings page.
* New: Added platform icon for LG devices.
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
* Mobile App:
* Fix: Improved API security and validation when registering the Android app.
* Docker:
* Fix: Docker container not respecting the PUID and PGID environment variables.
* Other:
* Fix: Error creating self-signed certificates on Python 3.
* Fix: Tautulli login session cookie not set on the HTTP root path.
* New: Windows and MacOS app installers to install Tautulli without needing Python installed.

View File

@@ -1,41 +0,0 @@
# Contributing to Tautulli
## Pull Requests
If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request.
### Branches
All pull requests should be based on the `nightly` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/nightly -b FEATURE_NAME`. Use meaningful commit messages.
### Python Code
#### Compatibility
The code should work with Python 2.7.17 or Python 3.6+. Note that Tautulli runs on many different platforms.
Re-use existing code. Do not hesitate to add logging in your code. You can the logger module `plexpy.logger.*` for this. Web requests are invoked via `plexpy.request.*` and derived ones. Use these methods to automatically add proper and meaningful error handling.
#### Code conventions
Although Tautulli did not adapt a code convention in the past, we try to follow the [PEP8](http://legacy.python.org/dev/peps/pep-0008/) conventions for future code. A short summary to remind you (copied from http://wiki.ros.org/PyStyleGuide):
* 4 space indentation
* 80 characters per line
* `package_name`
* `ClassName`
* `method_name`
* `field_name`
* `_private_something`
* `self.__really_private_field`
* `_global`
#### Documentation
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
### HTML/Template code
#### Compatibility
HTML5 compatible browsers are targeted.
#### Conventions
* 4 space indentation
* `methodName`
* `variableName`
* `ClassName`

View File

@@ -1,23 +0,0 @@
FROM tautulli/tautulli-baseimage:python3
LABEL maintainer="Tautulli"
ARG BRANCH
ARG COMMIT
ENV TAUTULLI_DOCKER=True
ENV TZ=UTC
WORKDIR /app
RUN \
echo ${BRANCH} > /app/branch.txt && \
echo ${COMMIT} > /app/version.txt
COPY . /app
ENTRYPOINT [ "./start.sh" ]
VOLUME /config
EXPOSE 8181
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1

View File

@@ -20,31 +20,27 @@
import os
import sys
# Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
from future.builtins import str
import appdirs
import argparse
import datetime
import locale
import pytz
import signal
import shutil
import time
import threading
import tzlocal
import plexpy
from plexpy import common, config, database, helpers, logger, webstart
import jellypy
from jellypy import common, config, database, helpers, logger, webstart
if common.PLATFORM == 'Windows':
from plexpy import windows
from jellypy import windows
elif common.PLATFORM == 'Darwin':
from plexpy import macos
from jellypy import macos
# Register signals, such as CTRL + C
signal.signal(signal.SIGINT, plexpy.sig_handler)
signal.signal(signal.SIGTERM, plexpy.sig_handler)
signal.signal(signal.SIGINT, jellypy.sig_handler)
signal.signal(signal.SIGTERM, jellypy.sig_handler)
def main():
@@ -55,28 +51,28 @@ def main():
# Fixed paths to Tautulli
if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
plexpy.FROZEN = True
plexpy.FULL_PATH = os.path.abspath(sys.executable)
plexpy.PROG_DIR = sys._MEIPASS
jellypy.FROZEN = True
jellypy.FULL_PATH = os.path.abspath(sys.executable)
jellypy.PROG_DIR = sys._MEIPASS
else:
plexpy.FULL_PATH = os.path.abspath(__file__)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
jellypy.FULL_PATH = os.path.abspath(__file__)
jellypy.PROG_DIR = os.path.dirname(jellypy.FULL_PATH)
plexpy.ARGS = sys.argv[1:]
jellypy.ARGS = sys.argv[1:]
# From sickbeard
plexpy.SYS_PLATFORM = sys.platform
plexpy.SYS_ENCODING = None
jellypy.SYS_PLATFORM = sys.platform
jellypy.SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
jellypy.SYS_LANGUAGE, jellypy.SYS_ENCODING = locale.getdefaultlocale()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
plexpy.SYS_ENCODING = 'UTF-8'
if not jellypy.SYS_ENCODING or jellypy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
jellypy.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments
parser = argparse.ArgumentParser(
@@ -106,70 +102,73 @@ def main():
args = parser.parse_args()
if args.verbose:
plexpy.VERBOSE = True
jellypy.VERBOSE = True
if args.quiet:
plexpy.QUIET = True
jellypy.QUIET = True
# Do an intial setup of the logger.
# Require verbose for pre-initilization to see critical errors
logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)
logger.initLogger(console=not jellypy.QUIET, log_dir=False, verbose=True)
try:
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
jellypy.SYS_TIMEZONE = tzlocal.get_localzone()
except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e:
logger.error("Could not determine system timezone: %s" % e)
plexpy.SYS_TIMEZONE = pytz.UTC
jellypy.SYS_TIMEZONE = pytz.UTC
plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z')
jellypy.SYS_UTC_OFFSET = datetime.datetime.now(jellypy.SYS_TIMEZONE).strftime('%z')
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
plexpy.DOCKER = True
jellypy.DOCKER = True
if helpers.bool_true(os.getenv('TAUTULLI_SNAP', False)):
jellypy.SNAP = True
if args.dev:
plexpy.DEV = True
jellypy.DEV = True
logger.debug("Tautulli is running in the dev environment.")
if args.daemon:
if sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
logger.warn("Daemonizing not supported under Windows, starting normally")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
jellypy.DAEMON = True
jellypy.QUIET = True
if args.nofork:
plexpy.NOFORK = True
jellypy.NOFORK = True
logger.info("Tautulli is running as a service, it will not fork when restarted.")
if args.pidfile:
plexpy.PIDFILE = str(args.pidfile)
jellypy.PIDFILE = str(args.pidfile)
# If the pidfile already exists, plexpy may still be running, so
# If the pidfile already exists, jellypy may still be running, so
# exit
if os.path.exists(plexpy.PIDFILE):
if os.path.exists(jellypy.PIDFILE):
try:
with open(plexpy.PIDFILE, 'r') as fp:
with open(jellypy.PIDFILE, 'r') as fp:
pid = int(fp.read())
os.kill(pid, 0)
except IOError as e:
raise SystemExit("Unable to read PID file: %s", e)
try:
os.kill(pid, 0)
except OSError:
logger.warn("PID file '%s' already exists, but PID %d is " \
logger.warn("PID file '%s' already exists, but PID %d is "
"not running. Ignoring PID file." %
(plexpy.PIDFILE, pid))
(jellypy.PIDFILE, pid))
else:
# The pidfile exists and points to a live PID. plexpy may
# The pidfile exists and points to a live PID. jellypy may
# still be running, so exit.
raise SystemExit("PID file '%s' already exists. Exiting." %
plexpy.PIDFILE)
jellypy.PIDFILE)
# The pidfile is only useful in daemon mode, make sure we can write the
# file properly
if plexpy.DAEMON:
plexpy.CREATEPID = True
if jellypy.DAEMON:
jellypy.CREATEPID = True
try:
with open(plexpy.PIDFILE, 'w') as fp:
with open(jellypy.PIDFILE, 'w') as fp:
fp.write("pid\n")
except IOError as e:
raise SystemExit("Unable to write PID file: %s", e)
@@ -179,95 +178,107 @@ def main():
# Determine which data directory and config file to use
if args.datadir:
plexpy.DATA_DIR = args.datadir
elif plexpy.FROZEN:
plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
jellypy.DATA_DIR = args.datadir
elif jellypy.FROZEN:
jellypy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
else:
plexpy.DATA_DIR = plexpy.PROG_DIR
jellypy.DATA_DIR = jellypy.PROG_DIR
# Migrate Snap data dir
if jellypy.SNAP:
snap_common = os.environ['SNAP_COMMON']
old_data_dir = os.path.join(snap_common, 'Tautulli')
if os.path.exists(old_data_dir) and os.listdir(old_data_dir):
jellypy.SNAP_MIGRATE = True
logger.info("Migrating Snap user data.")
shutil.move(old_data_dir, jellypy.DATA_DIR)
if args.config:
config_file = args.config
else:
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
config_file = os.path.join(jellypy.DATA_DIR, config.FILENAME)
# Try to create the DATA_DIR if it doesn't exist
if not os.path.exists(plexpy.DATA_DIR):
if not os.path.exists(jellypy.DATA_DIR):
try:
os.makedirs(plexpy.DATA_DIR)
os.makedirs(jellypy.DATA_DIR)
except OSError:
raise SystemExit(
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
'Could not create data directory: ' + jellypy.DATA_DIR + '. Exiting....')
# Make sure the DATA_DIR is writeable
if not os.access(plexpy.DATA_DIR, os.W_OK):
if not os.access(jellypy.DATA_DIR, os.W_OK):
raise SystemExit(
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
'Cannot write to the data directory: ' + jellypy.DATA_DIR + '. Exiting...')
# Put the database in the DATA_DIR
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
jellypy.DB_FILE = os.path.join(jellypy.DATA_DIR, database.FILENAME)
# Move 'plexpy.db' to 'tautulli.db'
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
# Move 'jellypy.db' to 'tautulli.db'
if os.path.isfile(os.path.join(jellypy.DATA_DIR, 'jellypy.db')) and \
not os.path.isfile(os.path.join(jellypy.DATA_DIR, jellypy.DB_FILE)):
try:
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
os.rename(os.path.join(jellypy.DATA_DIR, 'jellypy.db'), jellypy.DB_FILE)
except OSError as e:
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
raise SystemExit("Unable to rename jellypy.db to tautulli.db: %s", e)
if plexpy.DAEMON:
plexpy.daemonize()
if jellypy.DAEMON:
jellypy.daemonize()
# Read config and start logging
plexpy.initialize(config_file)
jellypy.initialize(config_file)
# Start the background threads
plexpy.start()
jellypy.start()
# Force the http port if neccessary
if args.port:
plexpy.HTTP_PORT = args.port
logger.info('Using forced web server port: %i', plexpy.HTTP_PORT)
jellypy.HTTP_PORT = args.port
logger.info('Using forced web server port: %i', jellypy.HTTP_PORT)
else:
plexpy.HTTP_PORT = int(plexpy.CONFIG.HTTP_PORT)
jellypy.HTTP_PORT = int(jellypy.CONFIG.HTTP_PORT)
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if plexpy.CONFIG.ENABLE_HTTPS:
if jellypy.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this "
"module to enable HTTPS. HTTPS will be disabled.")
plexpy.CONFIG.ENABLE_HTTPS = False
jellypy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
webstart.start()
if common.PLATFORM == 'Windows':
if plexpy.CONFIG.SYS_TRAY_ICON:
plexpy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
plexpy.WIN_SYS_TRAY_ICON.start()
if jellypy.CONFIG.SYS_TRAY_ICON:
jellypy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
jellypy.WIN_SYS_TRAY_ICON.start()
windows.set_startup()
elif common.PLATFORM == 'Darwin':
macos.set_startup()
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT,
plexpy.HTTP_ROOT)
if jellypy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not jellypy.DEV:
jellypy.launch_browser(jellypy.CONFIG.HTTP_HOST, jellypy.HTTP_PORT,
jellypy.HTTP_ROOT)
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
if common.PLATFORM == 'Darwin' and jellypy.CONFIG.SYS_TRAY_ICON:
if not macos.HAS_PYOBJC:
logger.warn("The pyobjc module is missing. Install this "
"module to enable the MacOS menu bar icon.")
plexpy.CONFIG.SYS_TRAY_ICON = False
jellypy.CONFIG.SYS_TRAY_ICON = False
if plexpy.CONFIG.SYS_TRAY_ICON:
if jellypy.CONFIG.SYS_TRAY_ICON:
# MacOS menu bar icon must be run on the main thread and is blocking
# Start the rest of Tautulli on a new thread
threading.Thread(target=wait).start()
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
plexpy.MAC_SYS_TRAY_ICON.start()
thread = threading.Thread(target=wait)
thread.daemon = True
thread.start()
jellypy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
jellypy.MAC_SYS_TRAY_ICON.start()
else:
wait()
else:
@@ -279,29 +290,29 @@ def wait():
# Wait endlessly for a signal to happen
while True:
if not plexpy.SIGNAL:
if not jellypy.SIGNAL:
try:
time.sleep(1)
except KeyboardInterrupt:
plexpy.SIGNAL = 'shutdown'
jellypy.SIGNAL = 'shutdown'
else:
logger.info('Received signal: %s', plexpy.SIGNAL)
logger.info('Received signal: %s', jellypy.SIGNAL)
if plexpy.SIGNAL == 'shutdown':
plexpy.shutdown()
elif plexpy.SIGNAL == 'restart':
plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout':
plexpy.shutdown(restart=True, checkout=True)
elif plexpy.SIGNAL == 'reset':
plexpy.shutdown(restart=True, reset=True)
elif plexpy.SIGNAL == 'update':
plexpy.shutdown(restart=True, update=True)
if jellypy.SIGNAL == 'shutdown':
jellypy.shutdown()
elif jellypy.SIGNAL == 'restart':
jellypy.shutdown(restart=True)
elif jellypy.SIGNAL == 'checkout':
jellypy.shutdown(restart=True, checkout=True)
elif jellypy.SIGNAL == 'reset':
jellypy.shutdown(restart=True, reset=True)
elif jellypy.SIGNAL == 'update':
jellypy.shutdown(restart=True, update=True)
else:
logger.error('Unknown signal. Shutting down...')
plexpy.shutdown()
jellypy.shutdown()
plexpy.SIGNAL = None
jellypy.SIGNAL = None
if __name__ == "__main__":

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of Tautulli.
#
# Tautulli 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.
#
# Tautulli 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 Tautulli. If not, see <http://www.gnu.org/licenses/>.
from Tautulli import main
# Call main() from Tautulli.py
if __name__ == "__main__":
main()

View File

@@ -1,64 +1,33 @@
# Tautulli
# JellyPy ![Python](https://img.shields.io/badge/python-3.6%2b-blue?style=flat-square)
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
A python based web application for monitoring, analytics and notifications for [Jellyfin](https://jellyfin.org/).
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 [Tautulli](https://github.com/Tautulli/Tautulli) (v2.6.5 at the time).
## Features
JellyPy only supports Jellyfin. If you are running run Plex, head over to Tautulli.
* 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!!
## Status
## Preview
Working on getting basic functionality up. It's going to take some time, based on that Jellyfin's API is
not well documented (read as: not documented at all).
* [Full preview gallery available on our website](https://tautulli.com)
- [x] Login to Jellyfin
- [ ] Libraries/Media
- [ ] Activity
- [ ] History
- [ ] User
![Tautulli Homepage](https://tautulli.com/images/screenshots/activity-compressed.jpg?v=2)
## Major Differences compared to Tautulli
* Dropped Plex/PMS Support
* Dropped Google Analytics
* Dropped Python2 support
* Dropped import from varius abondonded projects
## Installation & Support
[![Python](https://img.shields.io/badge/python-2.7.17,%203.6,%203.7,%203.8-blue?style=flat-square)](https://python.org/downloads)
[![Docker Pulls](https://img.shields.io/docker/pulls/tautulli/tautulli?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
[![Docker Stars](https://img.shields.io/docker/stars/tautulli/tautulli?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
| --- | --- | --- | --- |
| Release | [![Release@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![Release Date@master](https://img.shields.io/github/release-date/Tautulli/Tautulli?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/releases/latest) | [![Release@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![Commits@beta](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/beta?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/beta) | [![Last Commits@nightly](https://img.shields.io/github/last-commit/Tautulli/Tautulli/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [![Commits@nightly](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) |
| Docker | [![Docker@master](https://img.shields.io/badge/docker-latest-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [![Docker@beta](https://img.shields.io/badge/docker-beta-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@beta](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [![Docker@nightly](https://img.shields.io/badge/docker-nightly-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@nightly](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
| Installer | [![Windows@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=windows&style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![MacOS@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=macos&style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![Installer Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Release/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Amaster) | [![Windows@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=windows&include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![MacOS@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=macos&include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![Installer Build@beta](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Release/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Abeta) | [![Installer Build@nightly](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Release/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Anightly) |
[![Wiki](https://img.shields.io/badge/github-wiki-black?style=flat-square)](https://github.com/Tautulli/Tautulli-Wiki/wiki)
[![Discord](https://img.shields.io/discord/183396325142822912?label=discord&style=flat-square&color=7289DA)](https://tautulli.com/discord)
[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/tautulli?label=reddit&style=flat-square&color=FF5700)](https://www.reddit.com/r/Tautulli/)
[![Plex Forums](https://img.shields.io/badge/plex%20forums-discussion-E5A00D?style=flat-square)](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242)
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
* Support is available on [Discord](https://tautulli.com/discord), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242).
## Issues & Feature Requests
[![Issues](https://img.shields.io/badge/github-issues-red?style=flat-square)](https://github.com/Tautulli/Tautulli-Issues)
[![Feathub](https://img.shields.io/badge/feathub-requests-lightgrey?style=flat-square)](https://feathub.com/Tautulli/Tautulli)
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
TODO
## License
[![License](https://img.shields.io/github/license/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/blob/master/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.
[![License](https://img.shields.io/github/license/Tautulli/Tautulli?style=flat-square)](https://git.harting.dev/anonfunc/JellyPy/src/branch/master/LICENSE)

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Display information
echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated."

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Parameter check
if [ -z "$1" ]; then

View File

@@ -5,7 +5,7 @@
<h4 class="modal-title">Import ${app} Database</h4>
</div>
<div class="modal-body" id="modal-text">
<form id="import_database" enctype="multipart/form-data" method="post" name="import_database">
<form id="import_database_form" enctype="multipart/form-data" method="post" name="import_database_form">
<input type="hidden" id="import_app" name="import_app" value="${app.lower()}" />
% if app in ('PlexWatch', 'Plexivity'):
<p class="help-block">
@@ -28,11 +28,11 @@
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_database_file" name="import_database_file" required>
</label>
<input id="import_database_file_name" type="text" class="form-control" disabled>
<input id="import_database_file_name" type="text" class="form-control" placeholder="tautulli.db" disabled>
</div>
</div>
</div>
<p class="help-block">Upload the ${app} database you wish to import.</p>
<p class="help-block">Upload the ${app} database file you wish to import.</p>
</div>
<div class="form-group">
<label for="import_database_path">Option 2: Browse for a Database File</label>
@@ -40,13 +40,13 @@
<div class="col-xs-12">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="import_database_path_browse">Browse</button>
<button class="btn btn-form" type="button" id="import_database_path_browse" data-toggle="browse" data-description="Database File" data-filter=".db" data-target="#import_database_path">Browse</button>
</span>
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" required disabled>
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" placeholder="tautulli.db" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the ${app} database you wish to import.</p>
<p class="help-block">Browse for the ${app} database file you wish to import.</p>
</div>
% if app == 'Tautulli':
<div class="form-group">
@@ -64,7 +64,6 @@
<li><strong>Merge</strong> will add all history and remove any duplicates from the imported database into the current database.</li>
<li><strong>Overwrite</strong> will replace all history in the current database with the imported database.</li>
</ul>
<p class="help-block">Note: Libraries, users, notification agents, newsletter agents, and registered mobile devices will also be imported</p>
</div>
<div class="checkbox">
<label>
@@ -72,6 +71,15 @@
</label>
<p class="help-block">Automatically create a backup of the current database before importing.</p>
</div>
<div class="form-group">
<label>Import Notes</label>
<p class="help-block">The following data will also be imported:</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li>Libraries and Users</li>
<li>Notification / Newsletter Agents</li>
<li>Registered Mobile Devices</li>
</ul>
</div>
% else:
<div class="form-group">
<label for="import_table_name">Table Name</label>
@@ -106,19 +114,9 @@
</div>
</div>
<script>
$('#import_database_path_browse').click(function () {
$('#browse-path-type').text('Databse File');
$('#browse-path-modal').modal('show');
browsePath(null, null, '.db');
});
$('#select-browse-file').click(function () {
$('#browse-path-modal').modal('hide');
$("#import_database_path").val($('#browse-path').val());
});
$('#import_database_file').change(function() {
$("#import_database_file").change(function() {
if ($(this)[0].files[0]) {
$('#import_database_file_name').val($(this)[0].files[0].name);
$("#import_database_file_name").val($(this)[0].files[0].name);
}
});
@@ -126,7 +124,7 @@
$(this).prop('disabled', true);
var app = $("#import_app").val();
var database_file = $('#import_database_file')[0].files[0];
var database_file = $("#import_database_file")[0].files[0];
var database_path = $("#import_database_path").val();
var method = $("#import_method").val();
var backup = $("#import_backup_db").is(':checked');

View File

@@ -1,8 +1,8 @@
<%
import plexpy
from plexpy import version
from plexpy.helpers import anon_url
from plexpy.notifiers import BROWSER_NOTIFIERS
import jellypy
from jellypy import version
from jellypy.helpers import anon_url
from jellypy.notifiers import BROWSER_NOTIFIERS
%>
<!doctype html>
@@ -13,8 +13,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
<link href="${http_root}css/selectize.min.css" rel="stylesheet" />
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">
@@ -22,21 +24,21 @@
${next.headIncludes()}
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
<!-- ICONS -->
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
</head>
<body class="content">
@@ -48,17 +50,19 @@
% if plexpy.UPDATE_AVAILABLE is None:
You are running an unknown version of Tautulli.<br />
% elif plexpy.UPDATE_AVAILABLE == 'release':
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
% elif plexpy.UPDATE_AVAILABLE == 'commit':
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank" rel="noreferrer">
newer version</a> of Tautulli is available!<br />
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
% endif
% if plexpy.INSTALL_TYPE == 'docker':
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
% elif plexpy.INSTALL_TYPE in ('windows', 'macos'):
<a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
% elif plexpy.INSTALL_TYPE == 'snap':
Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>
% elif plexpy.INSTALL_TYPE == 'macos':
<a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
% else:
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
% endif
@@ -136,7 +140,7 @@
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
<li role="separator" class="divider"></li>
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
<li><a href="support"><i class="fa fa-fw fa-comment"></i> Support</a></li>
<li role="separator" class="divider"></li>
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
@@ -202,7 +206,7 @@ ${next.modalIncludes()}
</div>
</div>
<div class="modal-footer">
<span id="incorrect-login" style="padding-right: 25px; display: none;">Incorrect username or password.</span>
<span id="sign-in-alert" style="padding-right: 25px; display: none;"></span>
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
</div>
<input type="hidden" id="admin_login" name="admin_login" value="1" />
@@ -230,35 +234,44 @@ ${next.modalIncludes()}
</div>
</div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
<li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
<li><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab">Crypto</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
<p>
Click the button below to continue to Patreon.
</p>
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="github-donation" style="text-align: center">
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
<p>
Click the button below to continue to GitHub.
</p>
<a href="${anon_url('https://github.com/sponsors/JonnyWong16')}" target="_blank" class="btn btn-sm btn-default" style="font-weight: 600;">
<a href="${anon_url('https://github.com/sponsors/JonnyWong16')}" target="_blank" rel="noreferrer" class="btn btn-sm btn-default" style="font-weight: 600;">
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i>&nbsp; Sponsor
</a>
</div>
<div role="tabpanel" class="tab-pane" id="patreon-donation" style="text-align: center">
<p>
Click the button below to continue to Patreon.
</p>
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank" rel="noreferrer">
<img src="images/become_a_patron_button.png" alt="Become a Patron" width="170" height="40">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
<p>
Click the button below to continue to PayPal.
</p>
<a href="${anon_url('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=6XPPKTDSX9QFL&lc=US&item_name=Tautulli&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted')}" target="_blank">
<a href="${anon_url('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=6XPPKTDSX9QFL&lc=US&item_name=Tautulli&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted')}" target="_blank" rel="noreferrer">
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="crypto-donation" style="text-align: center">
<p>
Click the button below to continue to Coinbase.
</p>
<a href="https://blankrefer.com/?https://commerce.coinbase.com/checkout/8a9fa08c-8a38-409e-9220-868124c4ba0c" target="_blank" rel="noreferrer" class="donate-with-crypto">
<span>Donate with Crypto</span>
</a>
</div>
</div>
</div>
<div class="modal-footer">
@@ -288,14 +301,17 @@ ${next.modalIncludes()}
</div>
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
<script src="${http_root}js/bootstrap.min.js"></script>
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
<script src="${http_root}js/moment-with-locales.min.js"></script>
<script src="${http_root}js/moment-duration-format.min.js"></script>
<script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/ipaddr.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/selectize.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/ajaxNotifications.js"></script>
<script>
% if _session['user_group'] == 'admin':
@@ -325,15 +341,17 @@ ${next.modalIncludes()}
if (result.update === null) {
msg = 'You are running an unknown version of Tautulli.<br />';
} else if (result.update === true && result.release === true) {
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />';
msg = 'A <a href="' + result.release_url + '" target="_blank" rel="noreferrer">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />';
} else if (result.update === true && result.release === false) {
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
msg = 'A <a href="' + result.compare_url + '" target="_blank" rel="noreferrer">newer version</a> of Tautulli is available!<br />' +
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />';
}
if (result.install_type === 'docker') {
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
msg += '<a href="' + result.release_url + '" target="_blank">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
} else if (result.install_type === 'snap') {
msg += 'Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>';
} else if (result.install_type === 'macos') {
msg += '<a href="' + result.release_url + '" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
} else {
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
}
@@ -441,12 +459,16 @@ ${next.modalIncludes()}
data: $(this).serialize(),
dataType: 'json',
statusCode: {
200: function() {
200: function(xhr, status) {
window.location = "${http_root}";
},
401: function() {
$('#incorrect-login').show();
$('#username').focus();
401: function(xhr, status) {
$('#sign-in-alert').text('Incorrect username or password.').show();
$('#username').focus();
},
429: function(xhr, status) {
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
}
},
complete: function() {

View File

@@ -0,0 +1,138 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">${title}</h4>
</div>
<div class="modal-body" id="modal-text">
<form id="import_config_form" enctype="multipart/form-data" method="post" name="import_config_form">
<div class="form-group">
<label for="import_config_file">Option 1: Upload a Configuration File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<label for="import_config_file" class="input-group-btn">
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_config_file" name="import_config_file" required>
</label>
<input id="import_config_file_name" type="text" class="form-control" placeholder="config.ini" disabled>
</div>
</div>
</div>
<p class="help-block">Upload the Tautulli configuration file you wish to import.</p>
</div>
<div class="form-group">
<label for="import_config_path">Option 2: Browse for a Configuration File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="import_config_path_browse" data-toggle="browse" data-description="Configuration File" data-filter=".ini" data-target="#import_config_path">Browse</button>
</span>
<input type="text" class="form-control" id="import_config_path" name="import_config_path" value="" placeholder="config.ini" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the Tautulli configuration file you wish to import.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="import_backup_config" id="import_backup_config" value="1" checked> Backup Current Configuration
</label>
<p class="help-block">Automatically create a backup of the current configuration before importing.</p>
</div>
<div class="form-group">
<label>Import Notes</label>
<p class="help-block">The following settings will <em>not</em> be imported:</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li>Git Path, Log / Backup / Cache Directory, Plex Logs Folder</li>
<li>Custom Newsletter Templates Folder, Newsletter Output Directory</li>
<li>HTTP Host / Port / Root / Username / Password</li>
<li>Enable HTTPS, HTTPS Certificate / Certificate Chain / Key</li>
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<div>
<span id="status-message" style="padding-right: 25px;"></span>
<input type="button" id="import_config" class="btn btn-bright" value="Import">
</div>
</div>
</div>
</div>
<script>
$("#import_config_file").change(function() {
if ($(this)[0].files[0]) {
$("#import_config_file_name").val($(this)[0].files[0].name);
}
});
$("#import_config").click(function() {
$(this).prop('disabled', true);
var config_file = $("#import_config_file")[0].files[0];
var config_path = $("#import_config_path").val();
var backup = $("#import_backup_config").is(':checked');
var content_type;
var process_data;
var data;
if (config_file) {
content_type = false;
process_data = false;
data = new FormData();
data.append('config_file', config_file);
data.append('backup', backup);
} else {
content_type = 'application/x-www-form-urlencoded; charset=UTF-8';
process_data = true;
data = {
config_path: config_path,
backup: backup
}
}
if (config_file) {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>&nbsp; Uploading config file...');
} else {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
}
$.ajax({
url: 'import_config',
type: 'POST',
data: data,
cache: false,
async: true,
contentType: content_type,
processData: process_data,
success: function(data) {
var msg;
if (data.result === 'success') {
msg = "<i class='fa fa-check'></i>&nbsp; " + data.message;
window.location.href = 'restart_import_config';
} else {
msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; " + data.message;
}
$("#status-message").html(msg);
$("#import_config_file").val(null);
$("#import_config_file_name").val('');
$("#import_config_path").val('');
},
error: function (xhr) {
var msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; Error (" + xhr.status + "): ";
if (xhr.status === 413) {
msg += "file is too large to upload"
} else {
msg += 'try again'
}
$("#status-message").html(msg);
},
complete: function(xhr) {
$("#import_config").prop('disabled', false);
}
});
});
</script>

View File

@@ -13,8 +13,8 @@ DOCUMENTATION :: END
import os
import sys
import plexpy
from plexpy import common, logger
from plexpy.helpers import anon_url
from jellypy import common, logger
from jellypy.helpers import anon_url
%>
<table class="config-info-table small-muted">
@@ -22,11 +22,11 @@ DOCUMENTATION :: END
% if plexpy.CURRENT_VERSION:
<tr>
<td>Git Branch:</td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}" target="_blank">${plexpy.CONFIG.GIT_BRANCH}</a></td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}" target="_blank" rel="noreferrer">${plexpy.CONFIG.GIT_BRANCH}</a></td>
</tr>
<tr>
<td>Git Commit Hash:</td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}" target="_blank">${plexpy.CURRENT_VERSION}</a></td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}" target="_blank" rel="noreferrer">${plexpy.CURRENT_VERSION}</a></td>
</tr>
% endif
<tr>
@@ -49,6 +49,10 @@ DOCUMENTATION :: END
<td>Cache Directory:</td>
<td>${plexpy.CONFIG.CACHE_DIR}</td>
</tr>
<tr>
<td>Export Directory:</td>
<td>${plexpy.CONFIG.EXPORT_DIR}</td>
</tr>
<tr>
<td>Newsletter Directory:</td>
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
@@ -74,19 +78,19 @@ DOCUMENTATION :: END
<tr>
<td class="top-line">Resources:</td>
<td class="top-line">
<a class="no-highlight" href="${anon_url('https://tautulli.com')}" target="_blank">Tautulli Website</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Source</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="issue">GitHub Issues</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Wiki</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="feature request">FeatHub Feature Requests</a>
<a class="no-highlight" href="${anon_url('https://tautulli.com')}" target="_blank" rel="noreferrer">Tautulli Website</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">GitHub Source</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" rel="noreferrer" data-id="issue">GitHub Issues</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">GitHub Wiki</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" rel="noreferrer" data-id="feature request">FeatHub Feature Requests</a>
</td>
</tr>
<tr>
<td>Support:</td>
<td>
<a class="no-highlight support-modal-link" href="${anon_url('https://tautulli.com/discord')}" target="_blank">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242')}" target="_blank">Plex Forums</a>
<a class="no-highlight support-modal-link" href="${anon_url('https://tautulli.com/discord')}" target="_blank" rel="noreferrer">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank" rel="noreferrer">Tautulli Subreddit</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242')}" target="_blank" rel="noreferrer">Plex Forums</a>
</td>
</tr>
</tbody>

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@ ul.ColVis_collection {
list-style: none;
width: 150px;
padding: 8px 8px 4px 8px;
margin: 10px 0px 0px 0px;
margin: 10px 0px 10px 0px;
background-color: #444;
overflow: hidden;
z-index: 2002;

File diff suppressed because it is too large Load Diff

View File

@@ -118,9 +118,7 @@ DOCUMENTATION :: END
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 300, fallback='cover', refresh=True)});"></div>
</a>
% elif data['media_type'] in ('photo', 'clip'):
% if data['extra_type']:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['art'].replace('/art', '/thumb') or data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
% elif data['parent_thumb']:
% if data['parent_thumb']:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
% else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
@@ -220,7 +218,7 @@ DOCUMENTATION :: END
<div class="sub-heading">Container</div>
<div class="sub-value" id="transcode_container-${sk}">
% if data['stream_container_decision'] == 'transcode':
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
Converting (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
% else:
Direct Play (${data['stream_container'].upper()})
% endif
@@ -399,7 +397,7 @@ DOCUMENTATION :: END
</div>
</div>
<div class="dashboard-activity-metadata-wrapper">
<a href="${user_href}" title="${data['friendly_name']}">
<a href="${user_href}" title="${data['username']}">
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
</a>
<div class="dashboard-activity-metadata-title-container">
@@ -410,6 +408,10 @@ DOCUMENTATION :: END
<i class="fa fa-fw fa-pause"></i>&nbsp;
% elif data['state'] == 'buffering':
<i class="fa fa-fw fa-spinner"></i>&nbsp;
% elif data['state'] == 'error':
<i class="fa fa-fw fa-exclamation-triangle"></i>&nbsp;
% else:
<i class="fa fa-fw fa-question-circle"></i>&nbsp;
% endif
</div>
<div class="dashboard-activity-metadata-title">
@@ -521,7 +523,7 @@ DOCUMENTATION :: END
% endif
</div>
<div class="dashboard-activity-metadata-user">
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a>
<a href="${user_href}" title="${data['username']}">${data['friendly_name']}</a>
</div>
</div>
</div>

View File

@@ -27,7 +27,7 @@ DOCUMENTATION :: END
</%doc>
<%!
from plexpy import helpers
from jellypy import helpers
%>
% if data != None:
@@ -115,21 +115,13 @@ DOCUMENTATION :: END
var msg = 'Are you REALLY sure you want to purge all history for the <strong>${data["section_name"]}</strong> library?<br>' +
'This is permanent and cannot be undone!';
var url = 'delete_all_library_history';
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
confirmAjaxCall(url, msg, { server_id: '${server_id}', section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
});
$('#undelete-library').click(function () {
var msg = 'Are you sure you want to undelete this user?';
var msg = 'Are you sure you want to undelete this library?';
var url = 'undelete_library';
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
});
$(document).ready(function() {
// Move #confirm-modal to parent container
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) {
$('#confirm-modal-purge').appendTo($('#edit-library-modal').parent());
}
$('#edit-library-modal > #confirm-modal-purge').remove();
});
</script>
% endif

View File

@@ -27,7 +27,7 @@ DOCUMENTATION :: END
</%doc>
<%!
from plexpy import helpers
from jellypy import helpers
%>
% if data != None:
@@ -134,13 +134,5 @@ DOCUMENTATION :: END
var url = 'undelete_user';
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
});
$(document).ready(function() {
// Move #confirm-modal-purge to parent container
if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) {
$('#confirm-modal-purge').appendTo($('#edit-user-modal').parent());
}
$('#edit-user-modal > #confirm-modal-purge').remove();
});
</script>
% endif

View File

@@ -0,0 +1,289 @@
<%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: export_modal.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
DOCUMENTATION :: END
</%doc>
<%
import jellypy
from jellypy import exporter
from jellypy.helpers import anon_url
export = exporter.Export()
thumb_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[0]])
art_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[1]])
%>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="info-modal-title">
${title}
</h4>
</div>
<div class="modal-body">
<form method="post" class="form" id="export_metadata_form">
<input type="hidden" id="export_section_id" name="export_section_id" value="${section_id or ''}" />
<input type="hidden" id="export_user_id" name="export_user_id" value="${user_id or ''}" />
<input type="hidden" id="export_rating_key" name="export_rating_key" value="${rating_key or ''}" />
<input type="hidden" id="export_media_type" name="export_media_type" value="${media_type or ''}" />
<input type="hidden" id="export_sub_media_type" name="export_sub_media_type" value="${sub_media_type or ''}" />
<input type="hidden" id="export_export_type" name="export_export_type" value="${export_type or ''}" />
<div class="form-group">
<label>Instructions</label>
<p class="help-block">
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Exporter-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">Exporter Guide</a> for more details about each option.
</p>
</div>
<div class="form-group">
<label for="export_file_format">Data File Format</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="export_file_format" name="export_file_format">
% for format in file_formats:
<option value="${format}">${format.upper()}</option>
% endfor
</select>
</div>
</div>
<p class="help-block">Select the export data file format.</p>
</div>
% if not rating_key:
<div class="checkbox">
<label>
<input type="checkbox" id="export_individual_files" name="export_individual_files" value="1"> Export Individual Files
</label>
<p class="help-block">Enable to export one file for each ${media_type} instead of a single file containing all ${media_type}s.</p>
</div>
% endif
<div class="form-group">
<label for="export_metadata_level">Metadata Export Level</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="export_metadata_level" name="export_metadata_level">
<option value="0">Level 0 - None / Custom</option>
<option value="1" selected>Level 1 - Basic Metadata</option>
<option value="2">Level 2 - Extended Metadata</option>
<option value="3">Level 3 - Advanced Metadata</option>
<option value="9">Level 9 - All Metadata</option>
</select>
</div>
</div>
<p class="help-block">Select the metadata export level. Higher levels include all fields from the lower levels.</p>
</div>
<div class="form-group">
<label for="export_custom_metadata_fields">Custom Metadata Fields</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="export_custom_metadata_fields" name="export_custom_metadata_fields" data-field_type="Metadata">
</div>
</div>
<p class="help-block">Add additional fields to the selected metadata export level.</p>
</div>
<div class="form-group">
<label for="export_media_info_level">Media Info Export Level</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="export_media_info_level" name="export_media_info_level">
<option value="0">Level 0 - None / Custom</option>
<option value="1" selected>Level 1 - Basic Media Info</option>
<option value="2">Level 2 - Extended Media Info</option>
<option value="3">Level 3 - Advanced Media Info</option>
<option value="9">Level 9 - All Media Info</option>
</select>
</div>
</div>
<p class="help-block">Select the media info export level. Higher levels include all fields from the lower levels.</p>
</div>
<div class="form-group">
<label for="export_custom_media_info_fields">Custom Media Info Fields</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="export_custom_media_info_fields" name="export_custom_media_info_fields" data-field_type="Media Info">
</div>
</div>
<p class="help-block">Add additional fields to the selected media info export level.</p>
</div>
<div class="form-group">
<label for="export_thumb_level">Poster and Cover Image Export Level</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="export_thumb_level" name="export_thumb_level">
<option value="0" selected>Level 0 - None / Custom</option>
<option value="1">Level 1 - Uploaded and Selected Posters and Covers Only</option>
<option value="2">Level 2 - Selected and Locked Posters and Covers Only</option>
<option value="9">Level 9 - All Selected Posters and Covers</option>
</select>
</div>
</div>
<p class="help-block">
Select the level to export poster and cover image files.<br>Note: Only applies to ${thumb_media_types}.
</p>
</div>
<div class="form-group">
<label for="export_art_level">Background Artwork Image Export Level</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="export_art_level" name="export_art_level">
<option value="0" selected>Level 0 - None / Custom</option>
<option value="1">Level 1 - Uploaded and Selected Artwork Only</option>
<option value="2">Level 2 - Selected and Locked Artwork Only</option>
<option value="9">Level 9 - All Selected Artwork</option>
</select>
</div>
</div>
<p class="help-block">
Select the level to export background artwork image files.<br>Note: Only applies to ${art_media_types}.
</p>
</div>
<p class="help-block">
Warning: Exporting images may take a long time! Images will be saved to a folder alongside the data file.
</p>
</form>
</div>
<div class="modal-footer">
<div>
<input type="button" class="btn btn-bright btn-ok" data-dismiss="modal" id="export_metadata" value="Export">
</div>
</div>
</div>
</div>
<script src="${http_root}js/selectize.plugin.disable-options.js"></script>
<script>
$('#export_metadata_form').submit(function(e) {
e.preventDefault();
})
var optgroups = (function () {
var optgroups = [];
for (var i = 0; i <= 9; i++) {
optgroups.push({$order: i+1, value: i});
}
return optgroups
})()
var $export_custom_fields = $('#export_custom_metadata_fields, #export_custom_media_info_fields').selectize({
plugins: {
'remove_button': {},
'disable_options': {
disableField: 'level'
}
},
maxItems: null,
valueField: 'field',
labelField: 'field',
sortField: 'field',
searchField: ['field'],
optgroupField: 'level',
optgroups: optgroups,
lockOptgroupOrder: true,
render: {
optgroup_header: function(data, escape) {
return '<div class="optgroup-header">' + escape(this.$input.data('field_type') + ' Level: ' + data.value) + '</div>';
},
option: function (item, escape) {
return '<div data-field="' + escape(item.field) + '" data-level="' + escape(item.level) + '">' + escape(item.field) +'</div>';
}
}
});
var export_custom_metadata_fields = $export_custom_fields[0].selectize;
var export_custom_media_info_fields = $export_custom_fields[1].selectize;
function setDisabledFields() {
var metadata_export_level = $('#export_metadata_level option:selected').val();
var media_info_export_level = $('#export_media_info_level option:selected').val();
export_custom_metadata_fields.setDisabledOptions([...Array(parseInt(metadata_export_level) + 1).keys()]);
export_custom_media_info_fields.setDisabledOptions([...Array(parseInt(media_info_export_level) + 1).keys()]);
}
$('#export_metadata_level, #export_media_info_level').on('change', setDisabledFields);
function getExportFields() {
$.ajax({
url: 'get_export_fields',
async: true,
data: {
media_type: $('#export_media_type').val(),
sub_media_type: $('#export_sub_media_type').val()
},
success: function (result) {
if (result) {
export_custom_metadata_fields.addOption(result.metadata_fields);
export_custom_media_info_fields.addOption(result.media_info_fields);
setDisabledFields();
}
}
})
}
getExportFields();
$('#export_file_format').on('change', function() {
if ($(this).val() === 'm3u8') {
$('#export_metadata_level').prop('disabled', true);
$('#export_media_info_level').prop('disabled', true);
$("#export_thumb_level").prop('disabled', true);
$("#export_art_level").prop('disabled', true);
export_custom_metadata_fields.disable();
export_custom_media_info_fields.disable();
} else {
$('#export_metadata_level').prop('disabled', false);
$('#export_media_info_level').prop('disabled', false);
$("#export_thumb_level").prop('disabled', false);
$("#export_art_level").prop('disabled', false);
export_custom_metadata_fields.enable();
export_custom_media_info_fields.enable();
}
})
$("#export_metadata").click(function() {
var section_id = $('#export_section_id').val();
var user_id = $('#export_user_id').val();
var rating_key = $('#export_rating_key').val();
var metadata_export_level = $('#export_metadata_level option:selected').val();
var media_info_export_level = $('#export_media_info_level option:selected').val();
var file_format = $('#export_file_format option:selected').val();
var thumb_level = $("#export_thumb_level option:selected").val();
var art_level = $("#export_art_level option:selected").val();
var custom_fields = [
$('#export_custom_metadata_fields').val(),
$('#export_custom_media_info_fields').val()
].filter(Boolean).join(',');
var export_type = $('#export_export_type').val()
var individual_files = $('#export_individual_files').is(':checked')
$.ajax({
url: 'export_metadata',
data: {
section_id: section_id,
user_id: user_id,
rating_key: rating_key,
metadata_level: metadata_export_level,
media_info_level: media_info_export_level,
file_format: file_format,
thumb_level: thumb_level,
art_level: art_level,
custom_fields: custom_fields,
export_type: export_type,
individual_files: individual_files
},
async: true,
success: function (data) {
if (data.result === 'success') {
$("#nav-tabs-export").click();
redrawExportTable();
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
} else {
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
}
}
});
});
</script>

View File

@@ -40,14 +40,14 @@
</div>
</div>
</div>
<div class='table-card-back'>
<ul class="nav nav-pills" role="tablist" id="graph-tabs">
<li role="presentation"><a href="#tabs-1" aria-controls="tabs-1" data-toggle="tab" role="tab">Plays by Period</a></li>
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" data-toggle="tab" role="tab">Stream Info</a></li>
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" data-toggle="tab" role="tab">Play Totals</a></li>
<div class="table-card-back">
<ul class="nav nav-list nav-pills" role="tablist" id="graph-tabs">
<li role="presentation"><a id="nav-tabs-plays" href="#tabs-plays" aria-controls="tabs-plays" data-toggle="tab" role="tab">Plays by Period</a></li>
<li role="presentation"><a id="nav-tabs-stream" href="#tabs-stream" aria-controls="tabs-stream" data-toggle="tab" role="tab">Stream Info</a></li>
<li role="presentation"><a id="nav-tabs-total" href="#tabs-total" aria-controls="tabs-total" data-toggle="tab" role="tab">Play Totals</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane" id="tabs-1">
<div role="tabpanel" class="tab-pane" id="tabs-plays">
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
@@ -123,7 +123,7 @@
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-2">
<div role="tabpanel" class="tab-pane" id="tabs-stream">
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-video-camera"></i> Daily Stream type breakdown <small>Last <span class="days">30</span> days</small></h4>
@@ -195,7 +195,7 @@
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-3">
<div role="tabpanel" class="tab-pane" id="tabs-total">
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-calendar"></i> Plays by month <small>Last <span class="months">12</span> months</small></h4>
@@ -225,8 +225,6 @@
</%def>
<%def name="javascriptIncludes()">
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/moment-duration-format.js"></script>
<script src="${http_root}js/highcharts/js/highcharts.js"></script>
<script src="${http_root}js/jquery.dataTables.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
@@ -341,14 +339,29 @@
var yaxis = getLocalStorage('graph_type', 'plays');
var current_day_range = getLocalStorage('graph_days', 30);
var current_month_range = getLocalStorage('graph_months', 12);
var current_tab = '#' + getLocalStorage('graph_tab', 'tabs-1');
var current_tab = '#' + getLocalStorage('graph_tab', 'tabs-plays');
// Update tab values from upgrading
switch (current_tab) {
case '#tabs-1':
current_tab = '#tabs-plays'
break
case '#tabs-2':
current_tab = '#tabs-stream'
break
case '#tabs-3':
current_tab = '#tabs-total'
break
default:
break
}
$('#yaxis-' + yaxis).prop('checked', true);
$('#yaxis-' + yaxis).closest('label').addClass('active');
$('#graph-days').val(current_day_range);
$('#graph-months').val(current_month_range);
$('#graph-tabs a[href="' + current_tab + '"]').closest('li').addClass('active');
$(current_tab).addClass('active');
$('#nav-' + current_tab.replace('#', '')).tab('show').trigger('show.bs.tab');
//$(current_tab).addClass('active');
$('.days').html(current_day_range);
@@ -469,7 +482,7 @@
}
});
$('#graph-tabs a[href="#tabs-1"]').tab('show')
$('#nav-tabs-plays').tab('show');
}
function loadGraphsTab2(time_range, yaxis) {
@@ -562,7 +575,7 @@
}
});
$('#graph-tabs a[href="#tabs-2"]').tab('show')
$('#nav-tabs-2').tab('show');
}
function loadGraphsTab3(time_range, yaxis) {
@@ -586,16 +599,16 @@
}
});
$('#graph-tabs a[href="#tabs-3"]').tab('show')
$('#nav-tabs-total').tab('show');
}
// Set initial state
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
// Tab1 opened
$('#graph-tabs a[href="#tabs-1"]').on('shown.bs.tab', function (e) {
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
setLocalStorage('graph_tab', current_tab.replace('#',''));
@@ -603,7 +616,7 @@
});
// Tab2 opened
$('#graph-tabs a[href="#tabs-2"]').on('shown.bs.tab', function (e) {
$('#nav-tabs-stream').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
setLocalStorage('graph_tab', current_tab.replace('#',''));
@@ -611,7 +624,7 @@
});
// Tab3 opened
$('#graph-tabs a[href="#tabs-3"]').on('shown.bs.tab', function (e) {
$('#nav-tabs-total').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
setLocalStorage('graph_tab', current_tab.replace('#',''));
@@ -624,8 +637,8 @@
forceMinMax($(this));
current_day_range = $(this).val();
setLocalStorage('graph_days', current_day_range);
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
$('.days').html(current_day_range);
});
@@ -635,25 +648,25 @@
forceMinMax($(this));
current_month_range = $(this).val();
setLocalStorage('graph_months', current_month_range);
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
$('.months').html(current_month_range);
});
// User changed
$('#graph-user').on('change', function() {
selected_user_id = $(this).val() || null;
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
});
// Y-axis changed
$('#yaxis-selection').on('change', function() {
yaxis = $('input[name=yaxis-options]:checked', '#yaxis-selection').val();
setLocalStorage('graph_type', yaxis);
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
});
function setGraphFormat(type) {

View File

@@ -117,7 +117,6 @@
<script src="${http_root}js/dataTables.colVis.js"></script>
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script>
$(document).ready(function () {

View File

@@ -104,7 +104,7 @@ DOCUMENTATION :: END
</div>
% elif stat_id == 'top_users':
<% user_href = page('user', row0['user_id']) if row0['user_id'] else '#' %>
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['user']}" class="hidden-xs">
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
</a>
% elif stat_id == 'top_platforms':
@@ -122,7 +122,7 @@ DOCUMENTATION :: END
% elif stat_id.startswith('popular'):
<span class="dashboard-stats-stats-units">users</span>
% elif stat_id == 'last_watched':
<span class="dashboard-stats-stats-units" id="last-watched-header-info">${row0['friendly_name']}</span>
<span class="dashboard-stats-stats-units" id="last-watched-header-info" title="${row0['user']}">${row0['friendly_name']}</span>
% elif stat_id == 'most_concurrent':
<span class="dashboard-stats-stats-units" id="most-concurrent-header-info">streams</span>
% endif
@@ -134,7 +134,7 @@ DOCUMENTATION :: END
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}"
data-rating_key="${row.get('rating_key')}" data-guid="${row.get('guid')}" data-title="${row.get('title')}"
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
data-user_id="${row.get('user_id')}" data-user="${row.get('user')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}" data-live="${row.get('live')}">
<div class="sub-list">${loop.index + 1}</div>
<div class="sub-value">
@@ -152,7 +152,7 @@ DOCUMENTATION :: END
</a>
% elif stat_id == 'top_users':
<% user_href = page('user', row['user_id']) if row['user_id'] else '#' %>
<a href="${user_href}" title="${row['friendly_name']}">
<a href="${user_href}" title="${row['user']}">
${row['friendly_name']}
</a>
% elif stat_id == 'top_platforms':

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -2,7 +2,7 @@
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="mstile-150x150.png?v=2.0.5"/>
<square150x150logo src="mstile-150x150.png?v=2.6.0"/>
<TileColor>#282a2d</TileColor>
</tile>
</msapplication>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -6,12 +6,12 @@
"scope": "../../",
"icons": [
{
"src": "android-chrome-192x192.png?v=2.0.5",
"src": "android-chrome-192x192.png?v=2.6.0",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-256x256.png?v=2.0.5",
"src": "android-chrome-256x256.png?v=2.6.0",
"sizes": "256x256",
"type": "image/png"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1 +1,32 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M5695 6555 c-135 -34 -244 -94 -342 -189 -40 -39 -73 -76 -73 -83 0 -7 -4 -13 -10 -13 -14 0 -87 -156 -106 -225 -22 -83 -26 -234 -8 -320 17 -79 86 -230 133 -288 l30 -39 -48 -71 c-39 -57 -159 -228 -251 -357 -69 -97 -398 -564 -416 -590 -13 -19 -60 -87 -105 -150 -45 -63 -107 -151 -138 -195 -30 -44 -59 -84 -63 -90 -7 -9 -251 -354 -346 -490 -92 -131 -173 -245 -175 -245 -1 0 -34 9 -72 21 -130 38 -325 31 -454 -18 -168 -63 -313 -196 -385 -354 -39 -87 -65 -183 -68 -256 0 -24 -3 -43 -4 -43 -2 0 -43 46 -91 102 -49 57 -100 117 -115 133 -14 17 -128 149 -253 295 -125 146 -251 292 -279 324 -56 65 -77 89 -108 126 -58 68 -152 178 -172 200 -12 14 -50 57 -83 96 l-61 71 27 44 c58 93 91 217 92 342 2 161 -38 294 -125 412 -133 181 -316 279 -542 292 -470 27 -833 -434 -699 -887 74 -251 275 -437 530 -490 132 -28 334 -6 421 45 l42 24 173 -197 c96 -108 186 -210 200 -227 15 -16 163 -187 330 -380 458 -529 491 -567 526 -605 18 -19 31 -35 30 -36 -6 -5 -265 -161 -277 -167 -8 -4 -34 -20 -58 -35 -194 -124 -634 -382 -651 -382 -12 0 -46 20 -75 44 -60 49 -180 112 -242 127 -21 5 -48 12 -59 15 -11 4 -65 9 -121 11 -81 4 -117 1 -182 -15 -261 -66 -462 -270 -528 -537 -10 -40 -11 -217 -2 -258 5 -23 11 -51 14 -61 29 -145 147 -312 284 -403 123 -82 224 -114 370 -118 83 -3 124 2 240 29 36 9 133 57 187 94 60 41 111 91 153 152 14 19 28 37 32 40 19 15 71 140 89 217 17 73 20 107 16 198 -4 61 -7 121 -9 134 -3 28 -46 0 482 321 179 108 379 228 444 265 104 59 120 65 133 52 13 -13 12 -22 -10 -78 -49 -123 -58 -165 -62 -262 -7 -149 25 -286 89 -383 47 -72 91 -128 125 -158 19 -17 39 -36 45 -42 27 -25 136 -94 150 -94 8 0 17 -4 20 -9 3 -5 16 -11 28 -14 13 -3 50 -12 83 -21 74 -19 278 -15 345 7 198 65 358 196 435 358 16 34 20 36 49 28 17 -4 49 -10 71 -14 22 -3 99 -16 170 -30 72 -13 144 -26 160 -29 28 -5 101 -18 170 -31 17 -3 80 -14 140 -25 61 -11 124 -22 140 -25 17 -4 49 -9 72 -12 40 -5 42 -7 48 -47 14 -98 29 -147 73 -235 36 -75 61 -110 121 -171 154 -154 280 -210 480 -213 134 -2 180 5 273 40 212 83 371 262 427 481 24 93 25 255 2 342 -64 241 -245 428 -481 501 -62 18 -97 23 -200 22 -107 0 -136 -4 -205 -26 -44 -15 -109 -43 -145 -64 -83 -48 -208 -171 -250 -245 -17 -32 -35 -60 -38 -61 -4 -2 -46 4 -93 13 -48 10 -104 20 -125 23 -22 3 -46 8 -54 11 -8 3 -33 7 -55 10 -38 5 -58 9 -122 21 -16 3 -53 10 -83 15 -30 6 -66 12 -79 15 -13 2 -103 19 -200 36 -169 30 -207 42 -196 60 10 16 -28 155 -62 224 -19 39 -54 96 -78 127 l-45 58 40 52 c96 125 143 266 143 433 1 164 -27 263 -108 391 -19 30 -35 57 -35 61 0 3 31 49 69 102 57 81 450 638 625 889 28 40 62 88 76 107 14 18 194 274 400 568 291 414 379 534 393 531 10 -2 27 -6 37 -9 78 -25 240 -29 338 -9 433 87 677 573 489 974 -93 200 -255 332 -478 389 -87 22 -227 25 -304 6z"/></g></svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="350.000000pt" height="350.000000pt" viewBox="0 0 350.000000 350.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,350.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1566 3489 c-433 -46 -867 -274 -1141 -601 -404 -481 -526 -1100
-334 -1688 91 -278 283 -569 498 -756 676 -589 1646 -589 2322 0 215 187 407
478 498 756 142 436 113 895 -84 1305 -320 666 -1027 1061 -1759 984z m1147
-604 c87 -36 146 -118 154 -214 10 -111 -39 -203 -137 -254 -49 -26 -63 -28
-131 -25 l-76 3 -109 -154 c-60 -85 -190 -269 -290 -409 l-181 -255 26 -46
c22 -38 26 -59 26 -121 0 -63 -5 -84 -29 -132 -27 -54 -28 -59 -13 -76 22 -24
47 -86 47 -117 0 -14 6 -28 13 -30 6 -3 91 -16 187 -30 157 -23 175 -24 183
-10 38 68 115 118 199 130 103 15 220 -51 268 -151 26 -52 29 -154 6 -207 -19
-48 -82 -114 -129 -138 -151 -77 -346 22 -373 189 -7 46 15 39 -222 74 -142
20 -155 21 -163 6 -65 -116 -225 -163 -347 -102 -116 58 -167 187 -126 323 8
29 13 55 11 57 -3 3 -65 -33 -138 -79 -74 -46 -162 -100 -196 -120 l-62 -38 6
-47 c11 -100 -46 -207 -136 -254 -43 -23 -66 -28 -121 -28 -77 0 -124 16 -175
62 -48 41 -76 99 -82 167 -7 72 9 129 50 183 85 112 256 132 372 44 l31 -24
174 109 c96 60 180 111 185 113 6 2 -2 16 -16 32 -35 39 -412 468 -414 471 0
1 -21 -5 -45 -13 -57 -20 -142 -14 -196 14 -162 84 -197 288 -71 419 102 108
291 101 386 -14 62 -75 78 -185 40 -273 l-21 -49 23 -28 c13 -16 102 -118 198
-227 l175 -198 20 61 c26 78 64 125 124 155 63 31 117 39 177 26 49 -11 51
-11 72 17 21 26 533 749 548 773 4 6 -4 28 -17 48 -88 133 -44 307 94 376 61
31 163 36 221 11z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,8 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<title>android</title>
<path fill="#fff" d="M31.944 21.318c5.556 0 11.113 0 16.67 0 0.042 0 0.084-0 0.126 0.001 0.548 0.012 0.554 0.012 0.554 0.555 0.002 2.526 0.001 5.052 0.001 7.577 0 5.789 0.003 11.577-0.002 17.365-0.001 1.197-0.344 2.274-1.205 3.155-0.759 0.777-1.671 1.191-2.753 1.22-0.757 0.019-1.515 0.011-2.273 0.016-0.772 0.005-0.774 0.006-0.774 0.751-0.001 2.505-0.032 5.010 0.013 7.514 0.024 1.305-0.386 2.363-1.302 3.29-1.214 1.23-3.457 1.485-4.769 0.396-1.051-0.873-1.725-1.978-1.715-3.423 0.019-2.547 0.010-5.093 0.003-7.64-0.003-1.010 0.144-0.869-0.858-0.876-1.158-0.008-2.315-0.005-3.473-0.001-0.829 0.003-0.76-0.103-0.76 0.794-0.002 2.505-0.027 5.010 0.010 7.514 0.019 1.278-0.377 2.325-1.281 3.235-1.199 1.208-3.371 1.494-4.716 0.437-1.067-0.838-1.779-1.932-1.77-3.386 0.017-2.61 0.005-5.219 0.005-7.829 0-0.147-0.008-0.295 0-0.442 0.013-0.24-0.092-0.339-0.334-0.335-0.736 0.012-1.473 0.002-2.209 0.022-0.575 0.015-1.129-0.058-1.673-0.251-1.682-0.597-2.691-2.017-2.737-3.858-0.063-2.566-0.031-5.135-0.035-7.703-0.007-5.304-0.010-10.608-0.016-15.912-0.001-0.568-0.017-1.136-0.018-1.704-0-0.464 0.006-0.472 0.494-0.479 0.989-0.013 1.978-0.023 2.968-0.023 4.609-0.002 9.219-0.001 13.829-0.001-0.001 0.006-0.001 0.014-0.001 0.021z"></path>
<path fill="#fff" d="M31.944 19.89c-5.535 0-11.071 0.002-16.606-0.002-0.717-0-0.772 0.153-0.687-0.747 0.189-2.003 0.58-3.948 1.437-5.784 1.041-2.228 2.47-4.152 4.433-5.648 0.864-0.658 1.646-1.43 2.624-1.932 0.216-0.111 0.25-0.23 0.129-0.443-0.363-0.64-0.715-1.286-1.059-1.937-0.441-0.835-0.877-1.674-1.302-2.518-0.247-0.491-0.206-0.765 0.103-0.941 0.342-0.194 0.625-0.077 0.892 0.415 0.721 1.329 1.429 2.664 2.142 3.997 0.069 0.13 0.141 0.258 0.215 0.386 0.226 0.39 0.228 0.394 0.671 0.218 2.478-0.987 5.051-1.43 7.715-1.338 2.143 0.074 4.214 0.501 6.214 1.273 0.118 0.045 0.241 0.081 0.35 0.142 0.186 0.102 0.303 0.067 0.405-0.126 0.534-1.023 1.075-2.043 1.617-3.062 0.297-0.557 0.592-1.115 0.908-1.66 0.189-0.325 0.514-0.408 0.809-0.253 0.292 0.153 0.366 0.43 0.175 0.817-0.39 0.79-0.791 1.575-1.204 2.353-0.383 0.725-0.789 1.438-1.18 2.159-0.19 0.351-0.181 0.348 0.158 0.573 1.666 1.102 3.266 2.297 4.577 3.814 1.895 2.192 3.115 4.723 3.574 7.598 0.119 0.746 0.175 1.503 0.266 2.254 0.038 0.311-0.097 0.421-0.393 0.394-0.146-0.014-0.295-0.002-0.442-0.002-5.514 0-11.028 0-16.543 0zM25.561 12.038c-0.063-1.117-0.623-1.553-1.433-1.566-0.833-0.014-1.419 0.462-1.455 1.603-0.025 0.776 0.66 1.407 1.463 1.409 0.79 0.001 1.421-0.64 1.424-1.445zM39.872 13.483c0.788-0.007 1.497-0.676 1.439-1.441-0.076-0.997-0.486-1.549-1.506-1.576-0.841-0.022-1.403 0.67-1.386 1.605 0.016 0.816 0.635 1.418 1.453 1.411z"></path>
<path fill="#fff" d="M50.587 32.655c0-2.715-0.003-5.429 0.001-8.143 0.003-1.77 0.853-2.959 2.453-3.698 0.717-0.331 1.433-0.52 2.172-0.287 0.794 0.251 1.537 0.649 2.123 1.273 0.519 0.552 0.839 1.207 0.944 1.957 0.052 0.374 0.082 0.754 0.083 1.131 0.005 5.282-0.005 10.564 0.010 15.846 0.004 1.249-0.402 2.288-1.278 3.179-1.245 1.267-3.35 1.546-4.76 0.479-1.076-0.815-1.719-1.943-1.745-3.342-0.019-1.010-0.013-2.020-0.014-3.030-0.002-1.789-0.001-3.578-0.001-5.366 0.004-0 0.008-0 0.012-0z"></path>
<path fill="#fff" d="M13.369 32.464c0 2.335-0.001 4.669 0.001 7.004 0 0.63 0.047 1.263 0.002 1.889-0.072 1.003-0.541 1.811-1.23 2.554-0.931 1.004-2.059 1.18-3.323 1.058-1.55-0.15-3.156-2.028-3.181-3.665-0.004-0.231-0.015-0.462-0.014-0.694 0.003-5.406 0.007-10.812 0.011-16.218 0.001-1.655 0.863-2.749 2.268-3.501 0.683-0.366 1.397-0.602 2.158-0.402 1.622 0.427 3.305 1.697 3.292 3.834-0.016 2.713-0.004 5.427-0.004 8.141 0.007-0 0.013-0 0.020 0z"></path>
<path fill="#fff" d="M46.73 40.88c-0.003 0-0.007 0-0.010 0-1.475 0-2.67-1.195-2.67-2.67s1.195-2.67 2.67-2.67c1.475 0 2.67 1.195 2.67 2.67v0c0 0 0 0 0 0 0 1.471-1.19 2.664-2.659 2.67h-0.001zM17.27 40.88c-1.475 0-2.67-1.195-2.67-2.67s1.195-2.67 2.67-2.67c1.475 0 2.67 1.195 2.67 2.67v0c0 0.003 0 0.007 0 0.010 0 1.469-1.191 2.66-2.66 2.66-0.003 0-0.007 0-0.011 0h0.001zM47.68 24.83l5.32-9.23c0.095-0.159 0.151-0.351 0.151-0.557 0-0.405-0.219-0.76-0.546-0.951l-0.005-0.003c-0.16-0.095-0.354-0.152-0.56-0.152-0.407 0-0.764 0.22-0.957 0.547l-0.003 0.005-5.38 9.34c-4.027-1.851-8.738-2.93-13.7-2.93s-9.673 1.079-13.909 3.016l0.209-0.086-5.39-9.34c-0.204-0.28-0.531-0.46-0.9-0.46-0.613 0-1.11 0.497-1.11 1.11 0 0.167 0.037 0.325 0.103 0.467l-0.003-0.007 5.33 9.23c-9.153 5.047-15.453 14.286-16.323 25.059l-0.007 0.111h64c-0.875-10.883-7.171-20.121-16.158-25.088l-0.162-0.082z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><g data-name="Layer 2"><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)" data-name="Layer 1"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -5,7 +5,7 @@
</%def>
<%def name="body()">
<% from plexpy import PLEX_SERVER_UP %>
<% from jellypy import PLEX_SERVER_UP %>
<div class="container-fluid">
% for section in config['home_sections']:
% if section == 'current_activity':
@@ -24,17 +24,13 @@
</div>
<div id="currentActivity">
% if PLEX_SERVER_UP:
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i>&nbsp; Checking for activity...</div>
% elif config['pms_is_cloud']:
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
% elif not config['first_run_complete']:
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
% else:
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin':
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
% endif
</div>
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is connecting to the Plex server...</div>
% endif
</div>
</div>
@@ -65,7 +61,7 @@
<div class="row">
<div class="col-md-12">
<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>&nbsp; Loading stats...</div>
<br>
</div>
</div>
@@ -84,7 +80,7 @@
<div class="row">
<div class="col-md-12">
<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>&nbsp; Loading stats...</div>
<br>
</div>
</div>
@@ -132,17 +128,12 @@
<div class="col-md-12">
<div id="recentlyAdded" style="margin-right: -15px;">
% if PLEX_SERVER_UP:
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i>&nbsp; Looking for new items...</div>
% elif config['pms_is_cloud']:
<div class="text-muted">Plex Cloud server is sleeping.</div>
% else:
<div class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin':
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
% endif
</div>
<div id="dashboard-no-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is connecting to your Plex server...</div>
% endif
<br>
</div>
</div>
</div>
@@ -179,10 +170,10 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Terminate Session</h4>
<h4 class="modal-title">Terminate Stream</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you sure you want to terminate this session?</p>
<p>Are you sure you want to terminate this stream?</p>
<p>
<strong>
<span id="terminate-user"></span><br />
@@ -221,6 +212,28 @@
</div>
</div>
</div>
<% from plexpy.helpers import anon_url %>
<div id="python2-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="python2-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Unable to Update</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Tautulli is still running using Python 2 and cannot be updated past v2.6.3.</p>
<p>Python 3 is required to continue receiving updates.</p>
<p>
<strong>Please see the <a href="${anon_url('https://github.com/Tautulli/Tautulli-Wiki/wiki/Upgrading-to-Python-3-%28Tautulli-v2.5%29')}" target="_blank" rel="noreferrer">wiki</a>
for instructions on how to upgrade to Python 3.</strong>
</p>
</div>
<div class="modal-footer">
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
</div>
</div>
</div>
</div>
% endif
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
@@ -229,8 +242,6 @@
</%def>
<%def name="javascriptIncludes()">
<% from plexpy import PLEX_SERVER_UP %>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
<script>
@@ -260,8 +271,43 @@
}
});
}
% if _session['user_group'] == 'admin':
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
% else:
var msg_settings = '';
% endif
var error_msg = 'There was an error communicating with your Plex Server.' + msg_settings;
% if 'current_activity' in config['home_sections'] or 'recently_added' in config['home_sections']:
var server_status;
server_status = setInterval(function() {
$.getJSON('server_status', function (data) {
if (data.connected === true) {
clearInterval(server_status);
% if 'current_activity' in config['home_sections']:
$('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i>&nbsp; Checking for activity...</div>');
activityConnected();
% endif
% if 'recently_added' in config['home_sections']:
$('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i>&nbsp; Looking for new items...</div>');
recentlyAddedConnected();
% endif
} else if (data.connected === false) {
clearInterval(server_status);
% if 'current_activity' in config['home_sections']:
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
% endif
% if 'recently_added' in config['home_sections']:
$('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>');
% endif
}
});
}, 1000);
% endif
</script>
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
% if 'current_activity' in config['home_sections']:
<script>
var defaultHandler = {
get: function(target, name) {
@@ -272,7 +318,7 @@
var create_instances = [];
var activity_ready = true;
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
function getCurrentActivity() {
activity_ready = false;
@@ -298,13 +344,8 @@
}
if (!(current_activity)) {
% if _session['user_group'] == 'admin':
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
% else:
var msg_settings = '';
% endif
$('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
return
}
@@ -377,6 +418,9 @@
case 'buffering':
state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;';
break;
case 'error':
state_icon = '<i class="fa fa-fw fa-exclamation-triangle"></i>&nbsp;';
break;
default:
state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;';
}
@@ -431,7 +475,7 @@
var transcode_container = '';
if (s.stream_container_decision === 'transcode') {
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
transcode_container = 'Converting (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
} else {
transcode_container = 'Direct Play (' + s.stream_container.toUpperCase() + ')';
}
@@ -546,7 +590,7 @@
}
// Update the progress bars, percent - 3 because of 3px padding-right
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
$('#buffer-bar-' + key).css({width: parseInt(s.transcode_progress) - 3 + '%'}).html(s.transcode_progress + '%')
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
if (s.live !== 1) {
var progress_bar = $('#progress-bar-' + key);
@@ -623,34 +667,36 @@
});
}
getCurrentActivity();
setInterval(function () {
if (!(create_instances.length) && activity_ready) {
getCurrentActivity();
}
}, ${config['home_refresh_interval'] * 1000});
function activityConnected() {
getCurrentActivity();
setInterval(function () {
if (!(create_instances.length) && activity_ready) {
getCurrentActivity();
}
}, ${config['home_refresh_interval'] * 1000});
setInterval(function(){
$('.progress_time_offset').each(function () {
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
var view_offset = parseInt($(this).data('view_offset'));
var stream_duration = parseInt($(this).data('stream_duration'));
var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false);
$(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration))
}
});
$('.progress-bar').each(function () {
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
var view_offset = parseInt($(this).data('view_offset'));
var stream_duration = parseInt($(this).data('stream_duration'));
var progress_percent = Math.floor(view_offset / stream_duration * 100);
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
}
});
}, 1000);
setInterval(function(){
$('.progress_time_offset').each(function () {
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
var view_offset = parseInt($(this).data('view_offset'));
var stream_duration = parseInt($(this).data('stream_duration'));
var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false);
$(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration))
}
});
$('.progress-bar').each(function () {
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
var view_offset = parseInt($(this).data('view_offset'));
var stream_duration = parseInt($(this).data('stream_duration'));
var progress_percent = Math.floor(view_offset / stream_duration * 100);
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
$(this).css({width: progress_percent - 3 + '%'}).html(progress_percent + '%')
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
}
});
}, 1000);
}
$('#currentActivity').on('click', '.external_ip-modal', function () {
$.get('get_ip_address_details', {
@@ -756,7 +802,7 @@
if (user_id) {
href = page('user', user_id);
}
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('user'));
} else if (stat_id === 'top_platforms') {
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
@@ -874,7 +920,7 @@
getLibraryStats();
</script>
% endif
% if 'recently_added' in config['home_sections'] and PLEX_SERVER_UP:
% if 'recently_added' in config['home_sections']:
<script>
function recentlyAdded(recently_added_count, recently_added_type) {
showMsg("Loading recently added items...", true, false, 0);
@@ -902,7 +948,9 @@
$('#recently-added-toggle-' + recently_added_type).closest('label').addClass('active');
$('#recently-added-count').val(recently_added_count);
recentlyAdded(recently_added_count, recently_added_type);
function recentlyAddedConnected() {
recentlyAdded(recently_added_count, recently_added_type);
}
function highlightAddedScrollerButton() {
var scroller = $("#recently-added-row-scroller");
@@ -994,4 +1042,16 @@
});
</script>
% endif
% if _session['user_group'] == 'admin':
<script>
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
if (urlParams.get('update') === 'python2') {
$("#python2-modal").modal({
backdrop: 'static',
keyboard: false
});
}
</script>
% endif
</%def>

View File

@@ -39,9 +39,9 @@ DOCUMENTATION :: END
from collections import defaultdict
import re
from plexpy import notifiers
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
from plexpy.helpers import page, get_percent
from jellypy import notifiers
from jellypy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
from jellypy.helpers import page, get_percent, cast_to_int
# Get audio codec file
def af(codec):
@@ -84,8 +84,10 @@ DOCUMENTATION :: END
%>
<div class="container-fluid">
<div class="row">
% if data['media_type'] not in ('photo_album', 'photo', 'playlist'):
<% fallback = 'art-live-full' if data['live'] else None %>
<div class="art-face" style="background-image:url(${page('pms_image_proxy', data['art'], data['rating_key'], 1920, 1080, fallback=fallback)})"></div>
% endif
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -150,6 +152,29 @@ DOCUMENTATION :: END
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
% elif data['media_type'] == 'photo_album':
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
% if data['parent_title']:
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
% endif
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] in ('photo', 'clip'):
<li class="hidden-xs hidden-sm"><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'playlist':
% if user_info.get('user_id'):
<li><a href="${page('user', user_info.get('user_id'))}">${user_info.get('friendly_name')}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
% elif data['section_id']:
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
% endif
<li class="active metadata-xml">${data['title']}</li>
% endif
</ul>
</div>
@@ -158,10 +183,13 @@ DOCUMENTATION :: END
<div class="summary-content-title-wrapper">
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track':
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
<% legacy = '&legacy=1' if data['media_type'] in ('photo_album', 'photo', 'clip') else '' %>
% if data['media_type'] in ('track', 'photo'):
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}${legacy}" target="_blank" rel="noreferrer" title="View on Plex Web">
% elif data['media_type'] == 'playlist':
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/playlist?key=%2Fplaylists%2F${data['rating_key']}" target="_blank" rel="noreferrer" title="View on Plex Web">
% elif not data['live']:
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}${legacy}" target="_blank" rel="noreferrer" title="View on Plex Web">
% endif
% if data['live']:
<div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live')});">
@@ -179,11 +207,14 @@ DOCUMENTATION :: END
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
% elif data['media_type'] in ('artist', 'album', 'track', 'playlist', 'photo_album', 'photo', 'clip'):
<div class="summary-poster-face-track" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 500, fallback='cover')});">
<div class="summary-poster-face-overlay">
<span></span>
</div>
% if data['media_type'] == 'playlist' and data['smart']:
<span class="smart-playlist-image" title="Smart Playlist"><i class="fa fa-cog"></i></span>
% endif
</div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
@@ -214,7 +245,7 @@ DOCUMENTATION :: END
<h3 class="hidden-xs">S${data['parent_media_index']} &middot; E${data['media_index']}</h3>
% endif
% endif
% elif data['media_type'] in ('movie', 'show', 'artist', 'collection'):
% elif data['media_type'] in ('movie', 'show', 'artist', 'collection', 'playlist', 'photo_album'):
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['media_type'] == 'season':
<h1>&nbsp;</h1><h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
@@ -230,26 +261,30 @@ DOCUMENTATION :: END
<h1><a href="${page('info', data['grandparent_rating_key'])}">${data['original_title'] or data['grandparent_title']}</a></h1>
<h2><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a> - ${data['title']}</h2>
<h3 class="hidden-xs">T${data['media_index']}</h3>
% elif data['media_type'] in ('photo', 'clip'):
<h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
<h2>${data['title']}</h2>
% endif
</div>
</div>
</div>
<div class="summary-content-wrapper">
<div class="col-md-9">
% if data['media_type'] == 'movie' or data['live']:
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
% elif data['media_type'] in ('show', 'season', 'collection'):
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
% elif data['media_type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;">
% elif data['media_type'] == 'artist' or data['media_type'] == 'album':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;">
% elif data['media_type'] == 'track':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 180px;">
% else:
<div class="summary-content-padding hidden-xs hidden-sm">
% endif
% if data['media_type'] in ('movie', 'episode', 'track'):
<%
padding_height = ''
if data['media_type'] == 'movie' or data['live']:
padding_height = 'height: 305px;'
elif data['media_type'] in ('show', 'season', 'collection'):
padding_height = 'height: 270px;'
elif data['media_type'] == 'episode':
padding_height = 'height: 70px;'
elif data['media_type'] in ('artist', 'album', 'playlist', 'photo_album', 'photo'):
padding_height = 'height: 150px;'
elif data['media_type'] in ('track', 'clip'):
padding_height = 'height: 180px;'
%>
<div class="summary-content-padding hidden-xs hidden-sm" style="${padding_height}">
% if data['media_type'] in ('movie', 'episode', 'track', 'clip'):
<div class="summary-content-media-info-wrapper">
% if data['media_type'] != 'track' and media_info['video_codec']:
<img class="summary-content-media-flag" title="${media_info['video_codec']}" src="${http_root}images/media_flags/video_codec/${media_info['video_codec'] | vf}.png" />
@@ -268,11 +303,17 @@ DOCUMENTATION :: END
</div>
<div class="summary-content">
<div class="summary-content-details-wrapper">
% if data['rating']:
% if data['rating_image']:
% if data['rating_image'].startswith('imdb://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
<% rating = data['rating'] or data['audience_rating'] %>
% if rating:
% if data['audience_rating_image']:
% if data['audience_rating_image'].startswith('imdb://'):
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
<span class="rating-image rating-imdb"><strong>${rating}</strong></span>
</div>
% endif
% if data['audience_rating_image'].startswith('themoviedb://'):
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
<span class="rating-image rating-themoviedb"><strong>${get_percent(rating, 10)}%</strong></span>
</div>
% endif
% if data['audience_rating_image'].startswith('rottentomatoes://'):
@@ -286,11 +327,24 @@ DOCUMENTATION :: END
</div>
% endif
% else:
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<i class="star-icon fa fa-star"></i> <strong>${get_percent(data['rating'], 10)}%</strong>
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
<i class="star-icon fa fa-star"></i> <strong>${get_percent(rating, 10)}%</strong>
</div>
% endif
% endif
<div class="summary-content-details-tag">
% if data['media_type'] in ('collection', 'playlist') and data['children_count']:
<%
if data['media_type'] == 'collection':
suffix = MEDIA_TYPE_HEADERS[data['sub_media_type']]
elif data['media_type'] == 'playlist':
suffix = MEDIA_TYPE_HEADERS[data['playlist_type']]
if data['children_count'] == 1:
suffix = suffix[:-1]
%>
Items <strong> ${data['children_count']} ${suffix} </strong>
% endif
</div>
<div class="summary-content-details-tag">
% if data['directors']:
Directed by <strong> ${data['directors'][0]}</strong>
@@ -310,6 +364,8 @@ DOCUMENTATION :: END
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
Released <strong> ${data['year']}</strong>
% elif data['media_type'] in ('photo', 'clip'):
Taken <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['media_type'] == 'collection':
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
% elif data['year']:
@@ -318,7 +374,7 @@ DOCUMENTATION :: END
</div>
<div class="summary-content-details-tag">
% if data['duration']:
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
Runtime <strong> <span id="runtime">${data['duration']}</span></strong>
% endif
</div>
<div class="summary-content-details-tag">
@@ -434,6 +490,17 @@ DOCUMENTATION :: END
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div>
</div>
</div>
% elif data['media_type'] == 'photo_album':
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span>Photo List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading photo list...</div>
</div>
</div>
% elif data['media_type'] == 'collection':
<div class="col-md-12">
<div class="table-card-header">
@@ -442,30 +509,36 @@ DOCUMENTATION :: END
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading movies list...</div>
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading collection items...</div>
</div>
</div>
<div id="collection-related-list-container" style="display: none;">
</div>
% endif
% if data['media_type'] != 'collection':
% elif data['media_type'] == 'playlist':
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
% if data['media_type'] in ('artist', 'album', 'track'):
<span>Play History for <strong>${data['title']}</strong></span>
% else:
<span>Watch History for <strong>${data['title']}</strong></span>
% endif
<span>${MEDIA_TYPE_HEADERS[data['playlist_type']]} List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading playlist items...</div>
</div>
</div>
% endif
<%
history_type = data['media_type'] in ('movie', 'show', 'season', 'episode', 'artist', 'album', 'track')
history_active = 'active' if history_type else ''
export_active = 'active' if not history_type else ''
%>
% if history_type and _session['user_group'] == 'admin':
<div class="col-md-12">
<div class="table-card-header">
<ul class="nav nav-list nav-pills" role="tablist">
<li class="${history_active}"><a id="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
<li class="${export_active}"><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
</ul>
<div class="button-bar">
% if _session['user_group'] == 'admin':
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect rows to delete. Data is deleted upon exiting delete mode.</div>
<div class="btn-group">
<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>&nbsp;
</div>
% if source == 'history':
<div class="btn-group">
<a href="update_metadata?rating_key=${data['rating_key']}&update=True" class="btn btn-danger btn-edit" id="fix-metadata">
@@ -505,37 +578,117 @@ DOCUMENTATION :: END
</button>
</div>
% endif
% endif
<div class="btn-group">
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
</div>
<div class="btn-group colvis-button-bar"></div>
</div>
</div>
<div class="table-card-back">
<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="date">Date</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="product">Product</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>
</table>
</div>
</div>
% endif
<div class="tab-content">
% if history_type:
<div role="tabpanel" class="tab-pane ${history_active}" id="tabs-history">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
% if data['media_type'] in ('artist', 'album', 'track'):
<span>Play History for <strong>${data['title']}</strong></span>
% else:
<span>Watch History for <strong>${data['title']}</strong></span>
% endif
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect rows to delete. Data is deleted upon exiting delete mode.</div>
<div class="btn-group">
<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>&nbsp;
</div>
% endif
<div class="btn-group">
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-history"></div>
</div>
</div>
<div class="table-card-back">
<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="date">Date</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="product">Product</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>
</table>
</div>
</div>
</div>
</div>
</div>
% endif
% if not data['live'] and _session['user_group'] == 'admin':
<div role="tabpanel" class="tab-pane ${export_active}" id="tabs-export">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span>Metadata Exports for <strong>${data['title']}</strong></span>
</div>
<div class="button-bar">
<div class="btn-group">
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
data-section_id="${data['section_id']}" data-rating_key="${data['rating_key']}"
data-media_type="${data['media_type']}" data-sub_media_type="${data['sub_media_type'] or data['playlist_type'] or ''}">
<i class="fa fa-file-export"></i> Export metadata
</button>
</div>
<div class="btn-group">
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
<i class="fa fa-refresh"></i> Refresh exports
</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
</div>
</div>
<div class="table-card-back">
<table class="display export_table" id="export_table-RK-${data['rating_key']}" width="100%">
<thead>
<tr>
<th align="left" id="timestamp">Exported At</th>
<th align="left" id="media_type_title">Media Type</th>
<th align="left" id="rating_key">Rating Key</th>
<th align="left" id="filename">Filename</th>
<th align="left" id="file_format">File Format</th>
<th align="left" id="metadata_level">Metadata Level</th>
<th align="left" id="media_info_level">Media Info Level</th>
<th align="left" id="media_info_level">Custom Fields</th>
<th align="left" id="file_size">File Size</th>
<th align="left" id="complete">Download</th>
<th align="left" id="delete">Delete</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
% endif
</div>
</div>
</div>
</div>
@@ -624,6 +777,8 @@ DOCUMENTATION :: END
</div>
</div>
% endif
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
</div>
</%def>
<%def name="javascriptIncludes()">
@@ -631,13 +786,14 @@ DOCUMENTATION :: END
<script src="${http_root}js/dataTables.colVis.js"></script>
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script>
% if metadata:
<%
data = defaultdict(None, **metadata)
history_user_id = '' if _session['user_group'] == 'admin' else _session['user_id']
%>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
% if data['live']:
<script>
function get_history() {
@@ -648,7 +804,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
guid: "${data['guid']}",
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
user_id: "${history_user_id}"
};
}
}
@@ -664,7 +820,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
grandparent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
user_id: "${history_user_id}"
};
}
}
@@ -680,7 +836,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
parent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
user_id: "${history_user_id}"
};
}
}
@@ -696,96 +852,43 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
rating_key: "${data['rating_key']}",
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
user_id: "${history_user_id}"
};
}
}
}
</script>
% endif
% if data['media_type'] != 'collection':
% if data['media_type'] in ('movie', 'show', 'season', 'episode', 'artist', 'album', 'track'):
<script>
$(document).ready(function () {
function loadHistoryTable() {
get_history();
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, 12] });
$(colvis.button()).appendTo('div.colvis-button-bar');
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-RK-${data["rating_key"]}', history_table);
}
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
$(document).ready(function () {
loadHistoryTable();
});
$("#refresh-history-list").click(function () {
history_table.draw();
});
// Send recently added notification
$('#send-recently-added-notification').on('click', function () {
var rating_key = $(this).data('id');
$('#send-recently-added-modal').modal();
$('#send-recently-added-modal').one('click', '#confirm-send-notification', function () {
$.ajax({
url: 'send_manual_on_created',
data: {
rating_key: rating_key,
notifier_id: $('#send-notification-notifier option:selected').val()
},
async: true,
success: function (data) {
if (data.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
} else {
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
}
}
});
});
});
</script>
% endif
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'photo_album', 'collection', 'playlist'):
<script>
$.ajax({
url: 'get_item_children',
type: 'GET',
async: true,
data: { rating_key : "${data['rating_key']}" },
data: {
rating_key: "${data['rating_key']}",
media_type: "${data['media_type']}"
},
complete: function(xhr, status) {
$("#children-list").html(xhr.responseText);
}
@@ -799,7 +902,7 @@ DOCUMENTATION :: END
type: 'GET',
async: true,
data: {
rating_key : "${data['rating_key']}",
rating_key: "${data['rating_key']}",
title: "${data['title']}"
},
complete: function(xhr, status) {
@@ -809,12 +912,29 @@ DOCUMENTATION :: END
</script>
% endif
<script>
$('.metadata-xml').on('tripleclick', function () {
openPlexXML("/library/metadata/${data['rating_key']}");
$(document).ready(function () {
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.nav-list #nav-' + hash.replace('#' + prefix, "")).tab('show').trigger('show.bs.tab');
}
// Change hash for page-reload
$('.nav-list a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
});
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
var airdate = $("#airdate")
var runtime = $("#runtime")
airdate.html(moment(airdate.text()).format('MMM DD, YYYY'));
runtime.html(humanDuration(runtime.text()));
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
$('#channel-icon').popover({
selector: '[data-toggle=popover]',
@@ -828,6 +948,127 @@ DOCUMENTATION :: END
}
});
</script>
% if _session['user_group'] == 'admin':
<script>
$("#toggle-export-modal").click(function() {
$.ajax({
url: 'export_metadata_modal',
data: {
section_id: $(this).data('section_id'),
rating_key: $(this).data('rating_key'),
media_type: $(this).data('media_type'),
sub_media_type: $(this).data('sub_media_type')
},
cache: false,
async: true,
complete: function(xhr, status) {
$("#export-modal").html(xhr.responseText);
}
});
});
function loadExportTable() {
// Build export table
export_table_options.ajax = {
url: 'get_export_list',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
rating_key: "${data['rating_key']}"
};
}
};
export_table = $('#export_table-RK-${data["rating_key"]}').DataTable(export_table_options);
export_table.columns([2, 7]).visible(false);
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-export');
clearSearchButton('export_table-RK-${data["rating_key"]}', export_table);
}
$('#nav-tabs-export').on('shown.bs.tab', function() {
if (typeof(export_table) === 'undefined') {
loadExportTable();
}
});
$(document).ready(function () {
if (!($('#tabs-history').length)) {
loadExportTable();
}
});
$("#refresh-export-table").click(function () {
export_table.draw();
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
// Send recently added notification
$('#send-recently-added-notification').on('click', function () {
var rating_key = $(this).data('id');
$('#send-recently-added-modal').modal();
$('#send-recently-added-modal').one('click', '#confirm-send-notification', function () {
$.ajax({
url: 'send_manual_on_created',
data: {
rating_key: rating_key,
notifier_id: $('#send-notification-notifier option:selected').val()
},
async: true,
success: function (data) {
if (data.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
} else {
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
}
}
});
});
});
$('.metadata-xml').on('tripleclick', function () {
openPlexXML("/library/metadata/${data['rating_key']}");
});
</script>
% endif
% if data.get('poster_url'):
<script>
$('#hosted-poster').popover({

View File

@@ -28,14 +28,15 @@ DOCUMENTATION :: END
% if data != None:
<%
from plexpy.helpers import page
from jellypy.helpers import cast_to_int, page
%>
% if data['children_count'] > 0:
<div class="item-children-wrapper">
<ul class="item-children-instance list-unstyled">
<% max_height ='max-height' if data['children_type'] in ('track', 'photo') or media_type == 'playlist' else '' %>
<ul class="item-children-instance ${max_height} list-unstyled">
% for child in data['children_list']:
% if child['rating_key']:
% if data['children_type'] == 'track':
% if data['children_type'] in ('track', 'photo') or media_type == 'playlist':
<li class="item-children-list-item">
% else:
<li>
@@ -123,37 +124,144 @@ DOCUMENTATION :: END
</h3>
</div>
% elif data['children_type'] == 'track':
% if loop.index % 2 == 0:
<div class="item-children-list-item-even">
<span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
% if child['original_title']:
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
<div class="item-children-list-item-${e}">
<span class="item-children-list-item-index">${child['media_index']}</span>
<span class="item-children-list-item-title">
<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['title']}
</span>
</a>
% if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span>
% endif
% endif
</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
<% f = 'h:mm:ss' if cast_to_int(child['duration']) >= 3600000 else 'm:ss' %>
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("${f}"));</script>
</span>
</div>
% else:
<div class="item-children-list-item-odd">
<span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
% if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span>
% endif
% elif data['children_type'] == 'photo':
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
<div class="item-children-list-item-${e}">
<span class="item-children-list-item-index">${loop.index + 1}</span>
<span class="item-children-list-item-title">
% if child['media_type'] == 'photo_album':
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-camera fa-fw"></i></span>
% elif child['media_type'] == 'clip':
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-video-camera fa-fw"></i></span>
% else:
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>
% endif
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['title']}
</span>
</a>
</span>
% if child['duration']:
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
<% f = 'h:mm:ss' if cast_to_int(child['duration']) >= 3600000 else 'm:ss' %>
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("${f}"));</script>
</span>
% endif
</div>
% elif media_type == 'playlist':
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
<div class="item-children-list-item-${e}">
<span class="item-children-list-item-index">${loop.index + 1}</span>
<span class="item-children-list-item-title">
% if child['media_type'] == 'movie':
<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')}" data-height="120" data-width="80">
${child['title']}
</span>
</a>
<span class="text-muted"> (${child['year']})</span>
% elif child['media_type'] == 'episode':
<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>
<a href="${page('info', child['grandparent_rating_key'])}" title="${child['grandparent_title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['grandparent_thumb'], child['grandparent_rating_key'], 300, 450, fallback='poster')}" data-height="120" data-width="80">
${child['grandparent_title']}
</span>
</a> -
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 450, fallback='poster')}" data-height="120" data-width="80">
${child['title']}
</span>
</a>
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">S${child['parent_media_index']}</a> &middot; <a class="no-highlight" href="${page('info', child['rating_key'])}" title="${child['title']}">E${child['media_index']}</a>)</span>
% elif child['media_type'] == 'track':
<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['title']}
</span>
</a> -
<a href="${page('info', child['grandparent_rating_key'])}" title="${child['grandparent_title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['grandparent_thumb'], child['grandparent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['grandparent_title']}
</span>
</a>
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>)</span>
% elif child['media_type'] == 'photo':
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['title']}
</span>
</a>
% if child['grandparent_title']:
- <a href="${page('info', child['grandparent_rating_key'])}" title="${child['grandparent_title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['grandparent_thumb'], child['grandparent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['grandparent_title']}
</span>
</a>
% endif
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>)</span>
% elif child['media_type'] == 'clip':
<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
${child['title']}
</span>
</a>
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>)</span>
% endif
</span>
% if child['duration']:
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<% f = 'h:mm:ss' if cast_to_int(child['duration']) >= 3600000 else 'm:ss' %>
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("${f}"));</script>
</span>
% endif
</div>
% endif
% endif
</li>
% endif
% endfor
</ul>
</div>
<script>
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
container: 'body'
});
$('body').popover({
selector: '[data-toggle="popover"]',
html: true,
container: 'body',
trigger: 'hover',
placement: 'right',
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
content: function () {
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
}
});
</script>
% endif
% endif

View File

@@ -28,8 +28,8 @@ DOCUMENTATION :: END
% if data != None:
<%
from plexpy.common import MEDIA_TYPE_HEADERS
from plexpy.helpers import page
from jellypy.common import MEDIA_TYPE_HEADERS
from jellypy.helpers import page
types = ('movie', 'show', 'artist', 'album')
%>
% for media_type in types:

View File

@@ -54,7 +54,7 @@ DOCUMENTATION :: END
% if data != None:
<%
from plexpy.helpers import page
from jellypy.helpers import page
%>
% if data['results_count'] > 0:
% if 'collection' in data['results_list'] and data['results_list']['collection']:
@@ -65,7 +65,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['collection']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin':
@@ -90,7 +90,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['movie']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin':
@@ -115,7 +115,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['show']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin':
@@ -140,7 +140,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['season']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin':
@@ -165,7 +165,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['episode']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face episode-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 500, 280, fallback='art')});"></div>
% if _session['user_group'] == 'admin':
@@ -191,7 +191,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['artist']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
% if _session['user_group'] == 'admin':
@@ -215,7 +215,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['album']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
% if _session['user_group'] == 'admin':
@@ -240,7 +240,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['track']:
<li>
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster">
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')});">
<div class="item-children-card-overlay">

File diff suppressed because one or more lines are too long

View File

@@ -790,6 +790,9 @@ ColVis.prototype = {
oStyle.top = oPos.top+"px";
oStyle.left = iDivX+"px";
var iDocWidth = $(document).width();
var iDocHeight = $(document).height();
document.body.appendChild( nBackground );
document.body.appendChild( nHidden );
document.body.appendChild( this.dom.catcher );
@@ -819,12 +822,17 @@ ColVis.prototype = {
var iDivWidth = $(nHidden).outerWidth();
var iDivHeight = $(nHidden).outerHeight();
var iDocWidth = $(document).width();
var iDivMarginTop = parseInt($(nHidden).css("marginTop"), 10);
var iDivMarginBottom = parseInt($(nHidden).css("marginBottom"), 10);
if ( iLeft + iDivWidth > iDocWidth )
{
nHidden.style.left = (iDocWidth-iDivWidth)+"px";
}
if ( iDivY + iDivHeight > iDocHeight )
{
nHidden.style.top = (oPos.top - iDivHeight - iDivMarginTop - iDivMarginBottom)+"px";
}
}
this.s.hidden = false;
@@ -846,7 +854,8 @@ ColVis.prototype = {
this.s.hidden = true;
$(this.dom.collection).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {
this.style.display = "none";
// this.style.display = "none";
document.body.removeChild( this );
} );
$(this.dom.background).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,482 +0,0 @@
/*! Moment Duration Format v1.3.0
* https://github.com/jsmreese/moment-duration-format
* Date: 2014-07-15
*
* Duration format plugin function for the Moment.js library
* http://momentjs.com/
*
* Copyright 2014 John Madhavan-Reese
* Released under the MIT license
*/
(function (root, undefined) {
// repeatZero(qty)
// returns "0" repeated qty times
function repeatZero(qty) {
var result = "";
// exit early
// if qty is 0 or a negative number
// or doesn't coerce to an integer
qty = parseInt(qty, 10);
if (!qty || qty < 1) { return result; }
while (qty) {
result += "0";
qty -= 1;
}
return result;
}
// padZero(str, len [, isRight])
// pads a string with zeros up to a specified length
// will not pad a string if its length is aready
// greater than or equal to the specified length
// default output pads with zeros on the left
// set isRight to `true` to pad with zeros on the right
function padZero(str, len, isRight) {
if (str == null) { str = ""; }
str = "" + str;
return (isRight ? str : "") + repeatZero(len - str.length) + (isRight ? "" : str);
}
// isArray
function isArray(array) {
return Object.prototype.toString.call(array) === "[object Array]";
}
// isObject
function isObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
// findLast
function findLast(array, callback) {
var index = array.length;
while (index -= 1) {
if (callback(array[index])) { return array[index]; }
}
}
// find
function find(array, callback) {
var index = 0,
max = array.length,
match;
if (typeof callback !== "function") {
match = callback;
callback = function (item) {
return item === match;
};
}
while (index < max) {
if (callback(array[index])) { return array[index]; }
index += 1;
}
}
// each
function each(array, callback) {
var index = 0,
max = array.length;
if (!array || !max) { return; }
while (index < max) {
if (callback(array[index], index) === false) { return; }
index += 1;
}
}
// map
function map(array, callback) {
var index = 0,
max = array.length,
ret = [];
if (!array || !max) { return ret; }
while (index < max) {
ret[index] = callback(array[index], index);
index += 1;
}
return ret;
}
// pluck
function pluck(array, prop) {
return map(array, function (item) {
return item[prop];
});
}
// compact
function compact(array) {
var ret = [];
each(array, function (item) {
if (item) { ret.push(item); }
});
return ret;
}
// unique
function unique(array) {
var ret = [];
each(array, function (_a) {
if (!find(ret, _a)) { ret.push(_a); }
});
return ret;
}
// intersection
function intersection(a, b) {
var ret = [];
each(a, function (_a) {
each(b, function (_b) {
if (_a === _b) { ret.push(_a); }
});
});
return unique(ret);
}
// rest
function rest(array, callback) {
var ret = [];
each(array, function (item, index) {
if (!callback(item)) {
ret = array.slice(index);
return false;
}
});
return ret;
}
// initial
function initial(array, callback) {
var reversed = array.slice().reverse();
return rest(reversed, callback).reverse();
}
// extend
function extend(a, b) {
for (var key in b) {
if (b.hasOwnProperty(key)) { a[key] = b[key]; }
}
return a;
}
// define internal moment reference
var moment;
if (typeof require === "function") {
try { moment = require('moment'); }
catch (e) {}
}
if (!moment && root.moment) {
moment = root.moment;
}
if (!moment) {
throw "Moment Duration Format cannot find Moment.js";
}
// moment.duration.format([template] [, precision] [, settings])
moment.duration.fn.format = function () {
var tokenizer, tokens, types, typeMap, momentTypes, foundFirst, trimIndex,
args = [].slice.call(arguments),
settings = extend({}, this.format.defaults),
// keep a shadow copy of this moment for calculating remainders
remainder = moment.duration(this);
// add a reference to this duration object to the settings for use
// in a template function
settings.duration = this;
// parse arguments
each(args, function (arg) {
if (typeof arg === "string" || typeof arg === "function") {
settings.template = arg;
return;
}
if (typeof arg === "number") {
settings.precision = arg;
return;
}
if (isObject(arg)) {
extend(settings, arg);
}
});
// types
types = settings.types = (isArray(settings.types) ? settings.types : settings.types.split(" "));
// template
if (typeof settings.template === "function") {
settings.template = settings.template.apply(settings);
}
// tokenizer regexp
tokenizer = new RegExp(map(types, function (type) {
return settings[type].source;
}).join("|"), "g");
// token type map function
typeMap = function (token) {
return find(types, function (type) {
return settings[type].test(token);
});
};
// tokens array
tokens = map(settings.template.match(tokenizer), function (token, index) {
var type = typeMap(token),
length = token.length;
return {
index: index,
length: length,
// replace escaped tokens with the non-escaped token text
token: (type === "escape" ? token.replace(settings.escape, "$1") : token),
// ignore type on non-moment tokens
type: ((type === "escape" || type === "general") ? null : type)
// calculate base value for all moment tokens
//baseValue: ((type === "escape" || type === "general") ? null : this.as(type))
};
}, this);
// unique moment token types in the template (in order of descending magnitude)
momentTypes = intersection(types, unique(compact(pluck(tokens, "type"))));
// exit early if there are no momentTypes
if (!momentTypes.length) {
return pluck(tokens, "token").join("");
}
// calculate values for each token type in the template
each(momentTypes, function (momentType, index) {
var value, wholeValue, decimalValue, isLeast, isMost;
// calculate integer and decimal value portions
value = remainder.as(momentType);
wholeValue = (value > 0 ? Math.floor(value) : Math.ceil(value));
decimalValue = value - wholeValue;
// is this the least-significant moment token found?
isLeast = ((index + 1) === momentTypes.length);
// is this the most-significant moment token found?
isMost = (!index);
// update tokens array
// using this algorithm to not assume anything about
// the order or frequency of any tokens
each(tokens, function (token) {
if (token.type === momentType) {
extend(token, {
value: value,
wholeValue: wholeValue,
decimalValue: decimalValue,
isLeast: isLeast,
isMost: isMost
});
if (isMost) {
// note the length of the most-significant moment token:
// if it is greater than one and forceLength is not set, default forceLength to `true`
if (settings.forceLength == null && token.length > 1) {
settings.forceLength = true;
}
// rationale is this:
// if the template is "h:mm:ss" and the moment value is 5 minutes, the user-friendly output is "5:00", not "05:00"
// shouldn't pad the `minutes` token even though it has length of two
// if the template is "hh:mm:ss", the user clearly wanted everything padded so we should output "05:00"
// if the user wanted the full padded output, they can set `{ trim: false }` to get "00:05:00"
}
}
});
// update remainder
remainder.subtract(wholeValue, momentType);
});
// trim tokens array
if (settings.trim) {
tokens = (settings.trim === "left" ? rest : initial)(tokens, function (token) {
// return `true` if:
// the token is not the least moment token (don't trim the least moment token)
// the token is a moment token that does not have a value (don't trim moment tokens that have a whole value)
return !(token.isLeast || (token.type != null && token.wholeValue));
});
}
// build output
// the first moment token can have special handling
foundFirst = false;
// run the map in reverse order if trimming from the right
if (settings.trim === "right") {
tokens.reverse();
}
tokens = map(tokens, function (token) {
var val,
decVal;
if (!token.type) {
// if it is not a moment token, use the token as its own value
return token.token;
}
// apply negative precision formatting to the least-significant moment token
if (token.isLeast && (settings.precision < 0)) {
val = (Math.floor(token.wholeValue * Math.pow(10, settings.precision)) * Math.pow(10, -settings.precision)).toString();
} else {
val = token.wholeValue.toString();
}
// remove negative sign from the beginning
val = val.replace(/^\-/, "");
// apply token length formatting
// special handling for the first moment token that is not the most significant in a trimmed template
if (token.length > 1 && (foundFirst || token.isMost || settings.forceLength)) {
val = padZero(val, token.length);
}
// add decimal value if precision > 0
if (token.isLeast && (settings.precision > 0)) {
decVal = token.decimalValue.toString().replace(/^\-/, "").split(/\.|e\-/);
switch (decVal.length) {
case 1:
val += "." + padZero(decVal[0], settings.precision, true).slice(0, settings.precision);
break;
case 2:
val += "." + padZero(decVal[1], settings.precision, true).slice(0, settings.precision);
break;
case 3:
val += "." + padZero(repeatZero((+decVal[2]) - 1) + (decVal[0] || "0") + decVal[1], settings.precision, true).slice(0, settings.precision);
break;
default:
throw "Moment Duration Format: unable to parse token decimal value.";
}
}
// add a negative sign if the value is negative and token is most significant
if (token.isMost && token.value < 0) {
val = "-" + val;
}
foundFirst = true;
return val;
});
// undo the reverse if trimming from the right
if (settings.trim === "right") {
tokens.reverse();
}
return tokens.join("");
};
moment.duration.fn.format.defaults = {
// token definitions
escape: /\[(.+?)\]/,
years: /[Yy]+/,
months: /M+/,
weeks: /[Ww]+/,
days: /[Dd]+/,
hours: /[Hh]+/,
minutes: /m+/,
seconds: /s+/,
milliseconds: /S+/,
general: /.+?/,
// token type names
// in order of descending magnitude
// can be a space-separated token name list or an array of token names
types: "escape years months weeks days hours minutes seconds milliseconds general",
// format options
// trim
// "left" - template tokens are trimmed from the left until the first moment token that has a value >= 1
// "right" - template tokens are trimmed from the right until the first moment token that has a value >= 1
// (the final moment token is not trimmed, regardless of value)
// `false` - template tokens are not trimmed
trim: "left",
// precision
// number of decimal digits to include after (to the right of) the decimal point (positive integer)
// or the number of digits to truncate to 0 before (to the left of) the decimal point (negative integer)
precision: 0,
// force first moment token with a value to render at full length even when template is trimmed and first moment token has length of 1
forceLength: null,
// template used to format duration
// may be a function or a string
// template functions are executed with the `this` binding of the settings object
// so that template strings may be dynamically generated based on the duration object
// (accessible via `this.duration`)
// or any of the other settings
template: function () {
var types = this.types,
dur = this.duration,
lastType = findLast(types, function (type) {
return dur._data[type];
});
// default template strings for each duration dimension type
switch (lastType) {
case "seconds":
return "h:mm:ss";
case "minutes":
return "d[d] h:mm";
case "hours":
return "d[d] h[h]";
case "days":
return "M[m] d[d]";
case "weeks":
return "y[y] w[w]";
case "months":
return "y[y] M[m]";
case "years":
return "y[y]";
default:
return "y[y] M[m] d[d] h:mm:ss";
}
}
};
})(this);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -145,7 +145,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
dataString = $(formID).serialize();
}
// Loader Image
var loader = $("<div class='msg ajaxLoader-" + url +"'><i class='fa fa-refresh fa-spin'></i>&nbsp; Saving...</div>");
var loader = $("<div class='msg ajaxLoader-" + url + "'><i class='fa fa-refresh fa-spin'></i>&nbsp; Saving...</div>");
// Data Success Message
var dataSucces = $(elem).data('success');
if (typeof dataSucces === "undefined") {
@@ -248,10 +248,10 @@ getBrowsePath = function (key, path, filter_ext) {
path: path,
filter_ext: filter_ext
},
success: function(data) {
success: function (data) {
deferred.resolve(data);
},
error: function() {
error: function () {
deferred.reject();
}
});
@@ -330,40 +330,33 @@ function humanTime(seconds) {
}
}
function humanTimeClean(seconds) {
var text;
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();
});
};
function getPercent(value1, value2) {
value1 = parseFloat(value1) | 0
value2 = parseFloat(value2) | 0
var percent = 0;
if (value1 !== 0 && value2 !== 0) {
percent = (value1 / value2) * 100
}
return Math.round(percent)
}
function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) {
var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0);
if (roundToMinute) {
return (seconds >= 30 ? (minutes + 1) : minutes);
} else {
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
}
var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0);
if (roundToMinute) {
return (seconds >= 30 ? (minutes + 1) : minutes);
} else {
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
}
} else {
if (roundToMinute) {
return '0';
@@ -372,6 +365,61 @@ function millisecondsToMinutes(ms, roundToMinute) {
}
}
}
function humanDuration(ms, sig = 'dhm', units = 'ms', return_seconds = 300000) {
var factors = {
d: 86400000,
h: 3600000,
m: 60000,
s: 1000,
ms: 1
}
ms = parseInt(ms);
var d, h, m, s;
if (ms > 0) {
if (return_seconds && ms < return_seconds) {
sig = 'dhms'
}
ms = ms * factors[units];
h = ms % factors['d'];
d = Math.trunc(ms / factors['d']);
m = h % factors['h'];
h = Math.trunc(h / factors['h']);
s = m % factors['m'];
m = Math.trunc(m / factors['m']);
ms = s % factors['s'];
s = Math.trunc(s / factors['s']);
var hd_list = [];
if (sig >= 'd' && d > 0) {
d = (sig === 'd' && h >= 12) ? d + 1 : d;
hd_list.push(d.toString() + ' day' + ((d > 1) ? 's' : ''));
}
if (sig >= 'dh' && h > 0) {
h = (sig === 'dh' && m >= 30) ? h + 1 : h;
hd_list.push(h.toString() + ' hr' + ((h > 1) ? 's' : ''));
}
if (sig >= 'dhm' && m > 0) {
m = (sig === 'dhm' && s >= 30) ? m + 1 : m;
hd_list.push(m.toString() + ' min' + ((m > 1) ? 's' : ''));
}
if (sig >= 'dhms' && s > 0) {
hd_list.push(s.toString() + ' sec' + ((s > 1) ? 's' : ''));
}
return hd_list.join(' ')
} else {
return '0'
}
}
// Our countdown plugin takes a callback, a duration, and an optional message
$.fn.countdown = function (callback, duration, message) {
// If no message is provided, we use an empty string
@@ -412,6 +460,7 @@ function getCookie(cname) {
}
return "";
}
var Accordion = function (el, multiple, close) {
this.el = el || {};
this.multiple = multiple || false;
@@ -448,6 +497,7 @@ function clearSearchButton(tableName, table) {
table.search('').draw();
});
}
// Taken from https://github.com/Hellowlol/HTPC-Manager
window.onerror = function (message, file, line) {
var e = {
@@ -456,7 +506,8 @@ window.onerror = function (message, file, line) {
'file': file,
'line': line
};
$.post("log_js_errors", e, function (data) { });
$.post("log_js_errors", e, function (data) {
});
};
$('*').on('click', '.refresh_pms_image', function (e) {
@@ -506,14 +557,11 @@ function forceMinMax(elem) {
var default_val = parseInt(elem.data('default'));
if (isNaN(val)) {
elem.val(default_val);
}
else if (min !== undefined && val < min) {
} else if (min !== undefined && val < min) {
elem.val(min);
}
else if (max !== undefined && val > max) {
} else if (max !== undefined && val > max) {
elem.val(max);
}
else {
} else {
elem.val(val);
}
}
@@ -522,14 +570,14 @@ function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
$.fn.slideToggleBool = function(bool, options) {
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
$.fn.slideToggleBool = function (bool, options) {
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
};
function openPlexXML(endpoint, plextv, params) {
var data = $.extend({endpoint: endpoint, plextv: plextv}, params);
$.getJSON('return_plex_xml_url', data, function(xml_url) {
window.open(xml_url, '_blank');
$.getJSON('return_plex_xml_url', data, function (xml_url) {
window.open(xml_url, '_blank');
});
}
@@ -555,16 +603,19 @@ function PopupCenter(url, title, w, h) {
function setLocalStorage(key, value, path) {
var key_path = key;
if (path !== false) {
key = key + '_' + window.location.pathname;
key_path = key_path + '_' + window.location.pathname;
}
localStorage.setItem(key, value);
localStorage.setItem(key_path, value);
}
function getLocalStorage(key, default_value, path) {
var key_path = key;
if (path !== false) {
key = key + '_' + window.location.pathname;
key_path = key_path + '_' + window.location.pathname;
}
var value = localStorage.getItem(key);
var value = localStorage.getItem(key_path);
if (value !== null) {
return value
} else if (default_value !== undefined) {
@@ -574,7 +625,7 @@ function getLocalStorage(key, default_value, path) {
}
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, function (c) {
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
return (c ^ cryptoObj.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
});
@@ -598,44 +649,44 @@ function getPlexHeaders() {
var plex_oauth_window = null;
const plex_oauth_loader = '<style>' +
'.login-loader-container {' +
'font-family: "Open Sans", Arial, sans-serif;' +
'position: absolute;' +
'top: 0;' +
'right: 0;' +
'bottom: 0;' +
'left: 0;' +
'}' +
'.login-loader-message {' +
'color: #282A2D;' +
'text-align: center;' +
'position: absolute;' +
'left: 50%;' +
'top: 25%;' +
'transform: translate(-50%, -50%);' +
'}' +
'.login-loader {' +
'border: 5px solid #ccc;' +
'-webkit-animation: spin 1s linear infinite;' +
'animation: spin 1s linear infinite;' +
'border-top: 5px solid #282A2D;' +
'border-radius: 50%;' +
'width: 50px;' +
'height: 50px;' +
'position: relative;' +
'left: calc(50% - 25px);' +
'}' +
'@keyframes spin {' +
'0% { transform: rotate(0deg); }' +
'100% { transform: rotate(360deg); }' +
'}' +
'.login-loader-container {' +
'font-family: "Open Sans", Arial, sans-serif;' +
'position: absolute;' +
'top: 0;' +
'right: 0;' +
'bottom: 0;' +
'left: 0;' +
'}' +
'.login-loader-message {' +
'color: #282A2D;' +
'text-align: center;' +
'position: absolute;' +
'left: 50%;' +
'top: 25%;' +
'transform: translate(-50%, -50%);' +
'}' +
'.login-loader {' +
'border: 5px solid #ccc;' +
'-webkit-animation: spin 1s linear infinite;' +
'animation: spin 1s linear infinite;' +
'border-top: 5px solid #282A2D;' +
'border-radius: 50%;' +
'width: 50px;' +
'height: 50px;' +
'position: relative;' +
'left: calc(50% - 25px);' +
'}' +
'@keyframes spin {' +
'0% { transform: rotate(0deg); }' +
'100% { transform: rotate(360deg); }' +
'}' +
'</style>' +
'<div class="login-loader-container">' +
'<div class="login-loader-message">' +
'<div class="login-loader"></div>' +
'<br>' +
'Redirecting to the Plex login page...' +
'</div>' +
'<div class="login-loader-message">' +
'<div class="login-loader"></div>' +
'<br>' +
'Redirecting to the Plex login page...' +
'</div>' +
'</div>';
function closePlexOAuthWindow() {
@@ -652,10 +703,10 @@ getPlexOAuthPin = function () {
url: 'https://plex.tv/api/v2/pins?strong=true',
type: 'POST',
headers: x_plex_headers,
success: function(data) {
success: function (data) {
deferred.resolve({pin: data.id, code: data.code});
},
error: function() {
error: function () {
closePlexOAuthWindow();
deferred.reject();
}
@@ -701,7 +752,7 @@ function PlexOAuth(success, error, pre) {
type: 'GET',
headers: x_plex_headers,
success: function (data) {
if (data.authToken){
if (data.authToken) {
closePlexOAuthWindow();
if (typeof success === "function") {
success(data.authToken)
@@ -717,8 +768,10 @@ function PlexOAuth(success, error, pre) {
}
},
complete: function () {
if (!plex_oauth_window.closed && polling === pin){
setTimeout(function() {poll()}, 1000);
if (!plex_oauth_window.closed && polling === pin) {
setTimeout(function () {
poll()
}, 1000);
}
},
timeout: 10000
@@ -733,7 +786,7 @@ function PlexOAuth(success, error, pre) {
}
function encodeData(data) {
return Object.keys(data).map(function(key) {
return Object.keys(data).map(function (key) {
return [key, data[key]].map(encodeURIComponent).join("=");
}).join("&");
}
@@ -758,17 +811,39 @@ function page(endpoint, ...args) {
function pms_image_proxy(img, rating_key, width, height, opacity, background, blur, fallback, refresh, clip, img_format) {
var params = {};
if (img != null) { params.img = img; }
if (rating_key != null) { params.rating_key = rating_key; }
if (width != null) { params.width = width; }
if (height != null) { params.height = height; }
if (opacity != null) { params.opacity = opacity; }
if (background != null) { params.background = background; }
if (blur != null) { params.blur = blur; }
if (fallback != null) { params.fallback = fallback; }
if (refresh != null) { params.refresh = true; }
if (clip != null) { params.clip = true; }
if (img_format != null) { params.img_format = img_format; }
if (img != null) {
params.img = img;
}
if (rating_key != null) {
params.rating_key = rating_key;
}
if (width != null) {
params.width = width;
}
if (height != null) {
params.height = height;
}
if (opacity != null) {
params.opacity = opacity;
}
if (background != null) {
params.background = background;
}
if (blur != null) {
params.blur = blur;
}
if (fallback != null) {
params.fallback = fallback;
}
if (refresh != null) {
params.refresh = true;
}
if (clip != null) {
params.clip = true;
}
if (img_format != null) {
params.img_format = img_format;
}
return params;
}
@@ -782,7 +857,9 @@ function info_page(rating_key, guid, history, live) {
params.rating_key = rating_key;
}
if (history) { params.source = 'history'; }
if (history) {
params.source = 'history';
}
return params;
}
@@ -790,7 +867,9 @@ function info_page(rating_key, guid, history, live) {
function library_page(section_id) {
var params = {};
if (section_id != null) { params.section_id = section_id; }
if (section_id != null) {
params.section_id = section_id;
}
return params;
}
@@ -798,8 +877,25 @@ function library_page(section_id) {
function user_page(user_id, user) {
var params = {};
if (user_id != null) { params.user_id = user_id; }
if (user != null) { params.user = user; }
if (user_id != null) {
params.user_id = user_id;
}
if (user != null) {
params.user = user;
}
return params;
}
MEDIA_TYPE_HEADERS = {
'movie': 'Movies',
'show': 'TV Shows',
'season': 'Seasons',
'episode': 'Episodes',
'artist': 'Artists',
'album': 'Albums',
'track': 'Tracks',
'video': 'Videos',
'audio': 'Tracks',
'photo': 'Photos'
}

View File

@@ -0,0 +1,87 @@
/**
* Plugin: "disable_options" (selectize.js)
* Copyright (c) 2013 Mondo Robot & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
* @authors Jake Myers <jmyers0022@gmail.com>, Vaughn Draughon <vaughn@rocksolidwebdesign.com>
*/
Selectize.define('disable_options', function(options) {
var self = this;
options = $.extend({
'disableField': '',
'disableOptions': []
}, options);
self.refreshOptions = (function() {
var original = self.refreshOptions;
return function() {
original.apply(this, arguments);
$.each(options.disableOptions, function(index, option) {
self.$dropdown_content.find('[data-' + options.disableField + '="' + String(option) + '"]').addClass('option-disabled');
});
};
})();
self.onOptionSelect = (function() {
var original = self.onOptionSelect;
return function(e) {
var value, $target, $option;
if (e.preventDefault) {
e.preventDefault();
e.stopPropagation();
}
$target = $(e.currentTarget);
if ($target.hasClass('option-disabled')) {
return;
}
return original.apply(this, arguments);
};
})();
self.disabledOptions = function() {
return options.disableOptions;
}
self.setDisabledOptions = function( values ) {
options.disableOptions = values
}
self.disableOptions = function( values ) {
if ( ! ( values instanceof Array ) ) {
values = [ values ]
}
values.forEach( function( val ) {
if ( options.disableOptions.indexOf( val ) == -1 ) {
options.disableOptions.push( val )
}
} );
}
self.enableOptions = function( values ) {
if ( ! ( values instanceof Array ) ) {
values = [ values ]
}
values.forEach( function( val ) {
var remove = options.disableOptions.indexOf( val );
if ( remove + 1 ) {
options.disableOptions.splice( remove, 1 );
}
} );
}
});

View File

@@ -0,0 +1,111 @@
collections_table_options = {
"destroy": true,
"language": {
"search": "Search: ",
"lengthMenu": "Show _MENU_ entries per page",
"info": "Showing _START_ to _END_ of _TOTAL_ collections",
"infoEmpty": "Showing 0 to 0 of 0 entries",
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
"emptyTable": "No data in table",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
},
"pagingType": "full_numbers",
"stateSave": true,
"stateDuration": 0,
"processing": false,
"serverSide": true,
"pageLength": 25,
"order": [0, 'asc'],
"autoWidth": false,
"scrollX": true,
"columnDefs": [
{
"targets": [0],
"data": "titleSort",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['ratingKey'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<a href="' + page('info', rowData['ratingKey']) + '"><i class="fa fa-blank fa-fw"></i>' + thumb_popover + '</a>');
}
},
"width": "50%",
"className": "no-wrap"
},
{
"targets": [1],
"data": "collectionMode",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var mode = '';
if (cellData === -1) {
mode = 'Library default';
} else if (cellData === 0) {
mode = 'Hide collection';
} else if (cellData === 1) {
mode = 'Hide items in this collection';
} else if (cellData === 2) {
mode = 'Show this collection and its items';
}
$(td).html(mode);
}
},
"width": "20%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "collectionSort",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var sort = '';
if (cellData === 0) {
sort = 'Release date';
} else if (cellData === 1) {
sort = 'Alphabetical';
}
$(td).html(sort);
}
},
"width": "20%",
"className": "no-wrap"
},
{
"targets": [3],
"data": "childCount",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var type = MEDIA_TYPE_HEADERS[rowData['subtype']] || '';
if (rowData['childCount'] == 1) {
type = type.slice(0, -1);
}
$(td).html(cellData + ' ' + type);
}
},
"width": "10%",
"className": "no-wrap"
}
],
"drawCallback": function (settings) {
// Jump to top of page
//$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
$('body').popover({
selector: '[data-toggle="popover"]',
html: true,
container: 'body',
trigger: 'hover',
placement: 'right',
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
content: function () {
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
}
});
},
"preDrawCallback": function(settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbsp; Fetching rows...";
showMsg(msg, false, false, 0);
},
"rowCallback": function (row, rowData, rowIndex) {
}
};

View File

@@ -0,0 +1,251 @@
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
$.ajax({
url: 'get_date_formats',
type: 'GET',
success: function (data) {
date_format = data.date_format;
time_format = data.time_format;
}
});
export_table_options = {
"destroy": true,
"language": {
"search": "Search: ",
"lengthMenu": "Show _MENU_ entries per page",
"info": "Showing _START_ to _END_ of _TOTAL_ export items",
"infoEmpty": "Showing 0 to 0 of 0 entries",
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
"emptyTable": "No data in table",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
},
"pagingType": "full_numbers",
"stateSave": true,
"stateDuration": 0,
"processing": false,
"serverSide": true,
"pageLength": 25,
"order": [0, 'desc'],
"autoWidth": false,
"scrollX": true,
"columnDefs": [
{
"targets": [0],
"data": "timestamp",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(moment(cellData, "X").format(date_format + ' ' + time_format));
}
},
"width": "8%",
"className": "no-wrap",
"searchable": false
},
{
"targets": [1],
"data": "media_type_title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "7%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "rating_key",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null) {
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
}
},
"width": "6%",
"className": "no-wrap"
},
{
"targets": [3],
"data": "title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var tooltip;
var filename;
if (!rowData['individual_files']) {
tooltip = '<span data-toggle="tooltip" title="Single File"><i class="fa fa-file-alt fa-fw"></i></span>';
filename = cellData + '.' + rowData['file_format']
} else {
tooltip = '<span data-toggle="tooltip" title="Multiple Files"><i class="fa fa-folder fa-fw"></i></span>';
filename = cellData
}
if (rowData['complete'] === 1 && rowData['exists'] && !rowData['individual_files']) {
$(td).html('<a href="view_export?export_id=' + rowData['export_id'] + '" target="_blank">' + tooltip + '&nbsp;' + filename + '</a>');
} else {
$(td).html(tooltip + '&nbsp;' + filename);
}
}
},
"width": "40%",
"className": "no-wrap"
},
{
"targets": [4],
"data": "file_format",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var images = '';
if (rowData['thumb_level'] || rowData['art_level']) {
images = ' + images';
}
$(td).html(cellData + images);
}
},
"width": "7%",
"className": "no-wrap"
},
{
"targets": [5],
"data": "metadata_level",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null) {
$(td).html(cellData);
}
},
"width": "6%",
"className": "no-wrap"
},
{
"targets": [6],
"data": "media_info_level",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null) {
$(td).html(cellData);
}
},
"width": "6%",
"className": "no-wrap"
},
{
"targets": [7],
"data": "custom_fields",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData.replace(/,/g, ', '));
}
},
"width": "6%",
"className": "datatable-wrap"
},
{
"targets": [8],
"data": "file_size",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '' && cellData !== null) {
$(td).html(humanFileSize(cellData));
}
},
"width": "6%",
"className": "no-wrap",
"searchable": false
},
{
"targets": [9],
"data": "complete",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === 1 && rowData['exists']) {
var tooltip_title = '';
var icon = '';
if (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) {
tooltip_title = 'Zip Archive';
icon = 'fa-file-archive';
} else {
tooltip_title = rowData['file_format'].toUpperCase() + ' File';
icon = 'fa-file-download';
}
var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download';
$(td).html('<button class="btn btn-xs btn-success pull-left" data-id="' + rowData['export_id'] + '"><span data-toggle="tooltip" data-placement="left" title="' + tooltip_title + '"><i class="fa ' + icon + ' fa-fw"></i> Download</span></button>');
} else if (cellData === 0) {
var percent = Math.min(getPercent(rowData['exported_items'], rowData['total_items']), 99)
$(td).html('<span class="btn btn-xs btn-dark pull-left export-processing" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-spinner fa-spin fa-fw"></i> ' + percent + '%</span>');
} else if (cellData === -1) {
$(td).html('<span class="btn btn-xs btn-dark pull-left" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-exclamation-circle fa-fw"></i> Failed</span>');
} else {
$(td).html('<span class="btn btn-xs btn-dark pull-left" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-question-circle fa-fw"></i> Not Found</span>');
}
},
"width": "7%",
"className": "export_download",
"searchable": false
},
{
"targets": [10],
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
if (rowData['complete'] !== 0) {
$(td).html('<button class="btn btn-xs btn-danger pull-left" data-id="' + rowData['export_id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
} else {
$(td).html('<span class="btn btn-xs btn-danger pull-left" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-trash-o fa-fw"></i> Delete</span>');
}
},
"width": "7%",
"className": "export_delete",
"searchable": false
}
],
"drawCallback": function (settings) {
// Jump to top of page
//$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
container: 'body'
});
if (export_processing_timer) {
clearTimeout(export_processing_timer);
}
if ($('.export-processing').length) {
export_processing_timer = setTimeout(redrawExportTable.bind(null, false), 2000);
}
},
"preDrawCallback": function(settings) {
if (!export_processing_timer) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbsp; Fetching rows...";
showMsg(msg, false, false, 0)
}
},
"rowCallback": function (row, rowData, rowIndex) {
if (rowData['complete'] === 0) {
$(row).addClass('current-activity-row');
}
}
};
$('.export_table').on('click', '> tbody > tr > td.export_download > button', function (e) {
var tr = $(this).closest('tr');
var row = export_table.row(tr);
var rowData = row.data();
e.preventDefault();
window.location.href = 'download_export?export_id=' + rowData['export_id'];
});
$('.export_table').on('click', '> tbody > tr > td.export_delete > button', function (e) {
var tr = $(this).closest('tr');
var row = export_table.row(tr);
var rowData = row.data();
var msg = 'Are you sure you want to delete the following export?<br /><br /><strong>' + rowData['title'] + '</strong>';
var url = 'delete_export?export_id=' + rowData['export_id'];
confirmAjaxCall(url, msg, null, null, redrawExportTable);
});
function redrawExportTable(paging) {
export_table.draw(paging);
}
var export_processing_timer;

View File

@@ -55,13 +55,17 @@ history_table_options = {
if (rowData['state'] !== null) {
var state = '';
if (rowData['state'] === 'playing') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Playing"><i class="fa fa-play fa-fw"></i></span>';
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Playing"><i class="fa fa-fw fa-play"></i></span>';
} else if (rowData['state'] === 'paused') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-pause fa-fw"></i></span>';
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-fw fa-pause"></i></span>';
} else if (rowData['state'] === 'buffering') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-spinner fa-fw"></i></span>';
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-fw fa-spinner"></i></span>';
} else if (rowData['state'] === 'error') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Playback Error"><i class="fa fa-fw fa-exclamation-triangle"></i></span>';
} else if (rowData['state'] === 'stopped') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-fw fa-stop"></i></span>';
} else {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Unknown"><i class="fa fa-fw fa-question-circle"></i></span>';
}
$(td).html('<div><div style="float: left;">' + state + '&nbsp;' + date + '</div></div>');
} else if (rowData['group_count'] > 1) {
@@ -81,9 +85,9 @@ history_table_options = {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id']) {
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
} else {
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
}
} else {
$(td).html(cellData);
@@ -141,7 +145,7 @@ history_table_options = {
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>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream 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>';
}
@@ -184,7 +188,9 @@ history_table_options = {
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'clip') {
$(td).html(cellData);
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Clip"><i class="fa fa-video-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else {
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
}

View File

@@ -83,7 +83,7 @@ history_table_modal_options = {
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>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream 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>';
}

View File

@@ -192,7 +192,7 @@ libraries_list_table_options = {
"data": "duration",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html(humanTimeClean(cellData));
$(td).html(humanDuration(cellData, 'dhm', 's'));
}
},
"searchable": false,

View File

@@ -107,15 +107,15 @@ media_info_table_options = {
} else if (rowData['media_type'] === 'photo_album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'photo') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'clip') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 500, 280, null, null, null, 'art') + '" data-height="80" data-width="140">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else {
$(td).html(cellData);
}

View File

@@ -0,0 +1,100 @@
playlists_table_options = {
"destroy": true,
"language": {
"search": "Search: ",
"lengthMenu": "Show _MENU_ entries per page",
"info": "Showing _START_ to _END_ of _TOTAL_ playlists",
"infoEmpty": "Showing 0 to 0 of 0 entries",
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
"emptyTable": "No data in table",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
},
"pagingType": "full_numbers",
"stateSave": true,
"stateDuration": 0,
"processing": false,
"serverSide": true,
"pageLength": 25,
"order": [0, 'asc'],
"autoWidth": false,
"scrollX": true,
"columnDefs": [
{
"targets": [0],
"data": "title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var smart = '<i class="fa fa-blank fa-fw"></i>';
if (rowData['smart']) {
smart = '<span class="media-type-tooltip" data-toggle="tooltip" title="Smart Playlist"><i class="fa fa-cog fa-fw"></i></span>&nbsp;'
}
var breadcrumb = '';
if (rowData['userID']) {
breadcrumb = '&user_id=' + rowData['userID'];
} else if (rowData['librarySectionID']) {
breadcrumb = '&section_id=' + rowData['librarySectionID'];
}
var thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['composite'], rowData['ratingKey'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + smart + cellData + '</span>';
$(td).html('<a href="' + page('info', rowData['ratingKey']) + breadcrumb +'">' + thumb_popover + '</a>');
}
},
"width": "60%",
"className": "no-wrap"
},
{
"targets": [1],
"data": "leafCount",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var type = MEDIA_TYPE_HEADERS[rowData['playlistType']] || '';
if (rowData['leafCount'] === 1) {
type = type.slice(0, -1);
}
$(td).html(cellData + ' ' + type);
}
},
"width": "20%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "duration",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(humanDuration(cellData, 'dhm'));
}
},
"width": "20%",
"className": "no-wrap"
}
],
"drawCallback": function (settings) {
// Jump to top of page
//$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
container: 'body'
});
$('body').popover({
selector: '[data-toggle="popover"]',
html: true,
container: 'body',
trigger: 'hover',
placement: 'right',
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
content: function () {
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
}
});
},
"preDrawCallback": function(settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbsp; Fetching rows...";
showMsg(msg, false, false, 0);
$('[data-toggle="tooltip"]').tooltip('destroy');
},
"rowCallback": function (row, rowData, rowIndex) {
}
};

View File

@@ -51,9 +51,9 @@ sync_table_options = {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id']) {
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
} else {
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
}
} else {
$(td).html(cellData);

View File

@@ -1,3 +1,25 @@
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
$.ajax({
url: 'get_date_formats',
type: 'GET',
success: function(data) {
date_format = data.date_format;
time_format = data.time_format;
}
});
var seenRender = function (data, type, full) {
return moment(data, "X").fromNow();
};
var seenCreatedCell = function (td, cellData, rowData, row, col) {
if (cellData !== null) {
$(td).attr('title', moment(cellData, "X").format(date_format + ' ' + time_format));
}
};
user_ip_table_options = {
"destroy": true,
"language": {
@@ -21,16 +43,24 @@ user_ip_table_options = {
"columnDefs": [
{
"targets": [0],
"data":"last_seen",
"render": function ( data, type, full ) {
return moment(data, "X").fromNow();
},
"data": "last_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "15%",
"width": "12%",
"className": "no-wrap"
},
{
"targets": [1],
"data": "first_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "12%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
@@ -44,22 +74,22 @@ user_ip_table_options = {
$(td).html('n/a');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap modal-control-ip"
},
{
"targets": [2],
"targets": [3],
"data": "platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap"
},
{
"targets": [3],
"targets": [4],
"data": "player",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -67,18 +97,18 @@ user_ip_table_options = {
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>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream 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 + '&nbsp;' + cellData + '</div></a></div>');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap modal-control"
},
{
"targets": [4],
"targets": [5],
"data": "last_played",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -119,7 +149,7 @@ user_ip_table_options = {
"className": "datatable-wrap"
},
{
"targets": [5],
"targets": [6],
"data": "play_count",
"searchable": false,
"width": "10%",

View File

@@ -62,9 +62,9 @@ users_list_table_options = {
var inactive = '';
if (!rowData['is_active']) { inactive = '<span class="inactive-user-tooltip" data-toggle="tooltip" title="User not on Plex server"><i class="fa fa-exclamation-triangle"></i></span>'; }
if (cellData === '') {
$(td).html('<a href="' + page('user', rowData['user_id']) + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);">' + inactive + '</div></a>');
$(td).html('<a href="' + page('user', rowData['user_id']) + '"" title="' + rowData['username'] + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);">' + inactive + '</div></a>');
} else {
$(td).html('<a href="' + page('user', rowData['user_id']) + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');">' + inactive + '</div></a>');
$(td).html('<a href="' + page('user', rowData['user_id']) + '"" title="' + rowData['username'] + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');">' + inactive + '</div></a>');
}
},
"orderable": false,
@@ -78,7 +78,7 @@ users_list_table_options = {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html('<div class="edit-user-name" data-id="' + rowData['row_id'] + '">' +
'<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>' +
'<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>' +
'<input type="text" class="hidden" value="' + cellData + '">' +
'</div>');
} else {
@@ -142,7 +142,7 @@ users_list_table_options = {
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>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream 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>';
}
@@ -212,7 +212,7 @@ users_list_table_options = {
"data": "duration",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html(humanTimeClean(cellData));
$(td).html(humanDuration(cellData, 'dhm', 's'));
}
},
"searchable": false,

View File

@@ -38,7 +38,7 @@
<th align="left" id="count">Total Movies / TV Shows / Artists</th>
<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_accessed">Last Streamed</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 Played Duration</th>
@@ -79,7 +79,6 @@
<script src="${http_root}js/dataTables.colVis.js"></script>
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/libraries.js${cache_param}"></script>
<script>
$(document).ready(function () {

View File

@@ -36,8 +36,8 @@ DOCUMENTATION :: END
<%def name="body()">
% if data:
<%
from plexpy.common import LIVE_TV_SECTION_ID
from plexpy.helpers import page
from jellypy.common import LIVE_TV_SECTION_ID
from jellypy.helpers import page
%>
<div class="container-fluid">
<div class="row">
@@ -87,12 +87,19 @@ DOCUMENTATION :: END
% endif
</div>
<div class="user-info-nav">
<ul class="user-info-nav" role="tablist">
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
% if _session['user_group'] == 'admin':
<ul class="nav nav-list nav-pills" role="tablist">
<li class="active"><a id="nav-tabs-profile" href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
% if data['section_id'] != LIVE_TV_SECTION_ID:
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
% if _session['user_group'] == 'admin':
<li><a id="nav-tabs-mediainfo" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
% endif
% if data['section_type'] != 'artist':
<li><a id="nav-tabs-collections" href="#tabs-collections" role="tab" data-toggle="tab">Collections</a></li>
% endif
<li><a id="nav-tabs-playlists" href="#tabs-playlists" role="tab" data-toggle="tab">Playlists</a></li>
% if _session['user_group'] == 'admin':
<li><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
% endif
% endif
</ul>
@@ -242,23 +249,22 @@ DOCUMENTATION :: END
</div>
</div>
</div>
% if _session['user_group'] == 'admin':
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
% if config['get_file_sizes'] and data['section_id'] in config['get_file_sizes_hold']['section_ids']:
<div id="get_file_sizes_message" style="text-align: center; margin-top: 20px;">
% else:
<div id="get_file_sizes_message" style="text-align: center; margin-top: 20px; display: none;">
% endif
<i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is calculating the file sizes for the library's media info. This could take a few minutes depending on the size of your library.
<br />
You may leave this page and check back later.
</div>
% endif
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-history"></i> Media Info for <strong>
<i class="fa fa-info-circle"></i> Media Info for <strong>
<span class="set-username">${data['section_name']}</span>
</strong>
</span>
@@ -305,6 +311,157 @@ DOCUMENTATION :: END
</div>
</div>
</div>
% endif
% if data['section_type'] != 'artist':
<div role="tabpanel" class="tab-pane" id="tabs-collections">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-folder-open"></i> Collections for <strong>
<span class="set-username">${data['section_name']}</span>
</strong>
</span>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
<div class="btn-group">
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
data-section_id="${data['section_id']}" data-media_type="collection" data-sub_media_type="${data['section_type']}"
data-export_type="collection">
<i class="fa fa-file-export"></i> Export collections
</button>
</div>
% endif
<div class="btn-group">
<button class="btn btn-dark refresh-collections-table-button" id="refresh-collections-table">
<i class="fa fa-refresh"></i> Refresh collections
</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-collections"></div>
</div>
</div>
<div class="table-card-back">
<table class="display collections_table" id="collections_table-SID-${data['section_id']}" width="100%">
<thead>
<tr>
<th align="left" id="collectionTitle">Collection Title</th>
<th align="left" id="collectionMode">Collection Mode</th>
<th align="left" id="collectionSort">Collection Sort</th>
<th align="left" id="collectionItems">Collection Items</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
% endif
<div role="tabpanel" class="tab-pane" id="tabs-playlists">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-list-alt"></i> Playlists for <strong>
<span class="set-username">${data['section_name']}</span>
</strong>
</span>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
<% playlist_sub_media_type = {'movie': 'video', 'show': 'video', 'artist': 'audio', 'photo': 'photo'} %>
<div class="btn-group">
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
data-section_id="${data['section_id']}" data-media_type="playlist" data-sub_media_type="${playlist_sub_media_type.get(data['section_type'])}"
data-export_type="playlist">
<i class="fa fa-file-export"></i> Export playlists
</button>
</div>
% endif
<div class="btn-group">
<button class="btn btn-dark refresh-playlists-table-button" id="refresh-playlists-table">
<i class="fa fa-refresh"></i> Refresh playlists
</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-playlists"></div>
</div>
</div>
<div class="table-card-back">
<table class="display playlists_table" id="playlists_table-SID-${data['section_id']}" width="100%">
<thead>
<tr>
<th align="left" id="playlistTitle">Playlist Title</th>
<th align="left" id="playlistLeafCount">Playlist Items</th>
<th align="left" id="playlistDuration">Playlist Duration</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
% if _session['user_group'] == 'admin':
<div role="tabpanel" class="tab-pane" id="tabs-export">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-file-export"></i> Metadata Exports for <strong>
<span class="set-username">${data['section_name']}</span>
</strong>
</span>
</div>
<div class="button-bar">
<div class="btn-group">
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
data-section_id="${data['section_id']}" data-media_type="${'photoalbum' if data['section_type'] == 'photo' else data['section_type']}"
data-export_type="all">
<i class="fa fa-file-export"></i> Export metadata
</button>
</div>
<div class="btn-group">
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
<i class="fa fa-refresh"></i> Refresh exports
</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
</div>
</div>
<div class="table-card-back">
<table class="display export_table" id="export_table-SID-${data['section_id']}" width="100%">
<thead>
<tr>
<th align="left" id="timestamp">Exported At</th>
<th align="left" id="media_type_title">Media Type</th>
<th align="left" id="rating_key">Rating Key</th>
<th align="left" id="filename">Filename</th>
<th align="left" id="file_format">File Format</th>
<th align="left" id="metadata_level">Metadata Level</th>
<th align="left" id="media_info_level">Media Info Level</th>
<th align="left" id="media_info_level">Custom Fields</th>
<th align="left" id="file_size">File Size</th>
<th align="left" id="complete">Download</th>
<th align="left" id="delete">Delete</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
% endif
</div>
</div>
</div>
@@ -335,8 +492,7 @@ DOCUMENTATION :: END
</%def>
<%def name="modalIncludes()">
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog"
aria-labelledby="edit-library-modal">
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="edit-library-modal">
</div>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
@@ -360,6 +516,8 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
</div>
</%def>
<%def name="javascriptIncludes()">
@@ -369,6 +527,9 @@ DOCUMENTATION :: END
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
% if data:
<% from plexpy.common import LIVE_TV_SECTION_ID %>
<%
history_user_id = '' if _session['user_group'] == 'admin' else _session['user_id']
%>
<script>
% if str(data['section_id']).isdigit():
var section_id = ${data['section_id']};
@@ -384,14 +545,18 @@ DOCUMENTATION :: END
var get_file_sizes = null;
% endif
</script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
<script src="${http_root}js/tables/collections_table.js${cache_param}"></script>
<script src="${http_root}js/tables/playlists_table.js${cache_param}"></script>
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
<script>
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
$(".inactive-library-tooltip").tooltip();
function loadHistoryTable() {
// Build watch history table
history_table_options.ajax = {
@@ -401,7 +566,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
section_id: section_id,
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
user_id: "${history_user_id}"
};
}
};
@@ -413,7 +578,7 @@ DOCUMENTATION :: END
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
}
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
$('#nav-tabs-history').on('shown.bs.tab', function() {
if (typeof(history_table) === 'undefined') {
loadHistoryTable();
}
@@ -422,97 +587,70 @@ DOCUMENTATION :: END
$("#refresh-history-list").click(function () {
history_table.draw();
});
$(".inactive-library-tooltip").tooltip();
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
media_info_table_options.ajax = {
url: 'get_library_media_info',
</script>
% if data['section_type'] != 'artist':
<script>
function loadCollectionsTable() {
// Build collections table
collections_table_options.ajax = {
url: 'get_collections_list',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
refresh: refresh_table
section_id: section_id
};
}
};
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
collections_table = $('#collections_table-SID-${data["section_id"]}').DataTable(collections_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');
var colvis = new $.fn.dataTable.ColVis(collections_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-collections');
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
clearSearchButton('collections_table-SID-${data["section_id"]}', collections_table);
}
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
if (typeof(media_info_table) === 'undefined') {
loadMediaInfoTable();
$('#nav-tabs-collections').on('shown.bs.tab', function() {
if (typeof(collections_table) === 'undefined') {
loadCollectionsTable();
}
});
$("#refresh-media-info-table").click(function () {
media_info_child_table = {};
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
refresh_table = false;
$("#refresh-collections-table").click(function () {
collections_table.draw();
});
$("#edit-library-tooltip").tooltip();
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
</script>
% endif
<script>
function loadPlaylistsTable() {
// Build playlists table
playlists_table_options.ajax = {
url: 'get_playlists_list',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id
};
}
});
});
};
playlists_table = $('#playlists_table-SID-${data["section_id"]}').DataTable(playlists_table_options);
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
var colvis = new $.fn.dataTable.ColVis(playlists_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-playlists');
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
});
}
clearSearchButton('playlists_table-SID-${data["section_id"]}', playlists_table);
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
$('#nav-tabs-playlists').on('shown.bs.tab', function() {
if (typeof(playlists_table) === 'undefined') {
loadPlaylistsTable();
}
});
% endif
$("#refresh-playlists-table").click(function () {
playlists_table.draw();
});
function recentlyWatched() {
// Populate recently watched
@@ -634,11 +772,11 @@ DOCUMENTATION :: END
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
$('.nav-list #nav-' + hash.replace('#' + prefix, "")).tab('show').trigger('show.bs.tab');
}
// Change hash for page-reload
$('.user-info-nav a').on('shown.bs.tab', function (e) {
$('.nav-list a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
@@ -664,5 +802,143 @@ DOCUMENTATION :: END
});
</script>
% if _session['user_group'] == 'admin':
<script>
function loadMediaInfoTable() {
// Build media info table
media_info_table_options.ajax = {
url: 'get_library_media_info',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
refresh: refresh_table
};
}
};
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-SID-${data["section_id"]}', media_info_table);
}
$('#nav-tabs-mediainfo').on('shown.bs.tab', function() {
if (typeof(media_info_table) === 'undefined') {
loadMediaInfoTable();
}
});
$("#refresh-media-info-table").click(function () {
media_info_child_table = {};
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
refresh_table = false;
});
function loadExportTable() {
// Build export table
export_table_options.ajax = {
url: 'get_export_list',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id
};
}
};
export_table = $('#export_table-SID-${data["section_id"]}').DataTable(export_table_options);
export_table.columns([7]).visible(false);
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-export');
clearSearchButton('export_table-SID-${data["section_id"]}', export_table);
}
$('#nav-tabs-export').on('shown.bs.tab', function() {
if (typeof(export_table) === 'undefined') {
loadExportTable();
}
});
$("#refresh-export-table").click(function () {
export_table.draw();
});
$("#edit-library-tooltip").tooltip();
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
}
});
});
$(".export-button").click(function() {
$.ajax({
url: 'export_metadata_modal',
data: {
section_id: $(this).data('section_id'),
media_type: $(this).data('media_type'),
sub_media_type: $(this).data('sub_media_type'),
export_type: $(this).data('export_type')
},
cache: false,
async: true,
complete: function(xhr, status) {
$("#export-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
</script>
% endif
% endif
</%def>

View File

@@ -32,7 +32,7 @@ DOCUMENTATION :: END
% if data:
<%
from plexpy.helpers import page
from jellypy.helpers import page
%>
<div class="dashboard-recent-media-row">
<div id="recently-added-row-scroller" style="left: 0;">

View File

@@ -25,7 +25,7 @@ DOCUMENTATION :: END
% if data:
<%
from plexpy.helpers import page
from jellypy.helpers import page
types = ('movie', 'show', 'artist', 'photo')
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),

View File

@@ -19,17 +19,17 @@ DOCUMENTATION :: END
</%doc>
% if data:
<% from plexpy.helpers import page %>
<% from jellypy.helpers import page %>
% for a in data:
<ul class="list-unstyled">
<div class="user-player-instance">
<li>
% if a['user_id']:
<a href="${page('user', a['user_id'])}" title="${a['friendly_name']}">
<a href="${page('user', a['user_id'])}" title="${a['username']}">
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>
</a>
<div class=" user-player-instance-name">
<a href="${page('user', a['user_id'])}" title="${a['friendly_name']}">${a['friendly_name']}</a>
<a href="${page('user', a['user_id'])}" title="${a['username']}">${a['friendly_name']}</a>
</div>
% else:
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>

View File

@@ -1,6 +1,6 @@
<%
import plexpy
plex_login = plexpy.CONFIG.HTTP_PLEX_ADMIN or plexpy.CONFIG.ALLOW_GUEST_ACCESS
import jellypy
plex_login = jellypy.CONFIG.HTTP_PLEX_ADMIN or jellypy.CONFIG.ALLOW_GUEST_ACCESS
%>
<!doctype html>
@@ -11,28 +11,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
<!-- ICONS -->
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
</head>
<body style="margin: 0; overflow: auto;">
@@ -109,7 +109,7 @@
</div>
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
<script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script>
@@ -159,16 +159,20 @@
data: data,
dataType: 'json',
statusCode: {
200: function() {
200: function(xhr, status) {
window.location = "${redirect_uri or http_root}";
},
401: function() {
401: function(xhr, status) {
if (plex) {
$('#sign-in-alert').text('Invalid Plex Login.').show();
} else {
$('#sign-in-alert').text('Incorrect username or password.').show();
$('#username').focus();
}
},
429: function(xhr, status) {
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
}
},
complete: function() {

View File

@@ -1,6 +1,6 @@
<%inherit file="base.html"/>
<%!
from plexpy import helpers
from jellypy import helpers
%>
<%def name="headIncludes()">
@@ -208,7 +208,6 @@
<script src="${http_root}js/jquery.dataTables.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/logs.js${cache_param}"></script>
<script src="${http_root}js/tables/plex_logs.js${cache_param}"></script>
<script src="${http_root}js/tables/notification_logs.js${cache_param}"></script>

View File

@@ -33,7 +33,7 @@
<label for="friendly_name">OneSignal Device ID</label>
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" id="device_id" value="${device['device_id']}" size="30" readonly>
<input type="text" class="form-control" id="onesignal_id" value="${device['onesignal_id'] or ''}" size="30" readonly>
</div>
</div>
<p class="help-block">Your OneSignal device ID for notifications.</p>

View File

@@ -121,6 +121,7 @@ DOCUMENTATION :: END
});
$('#api_qr_address').change(function () {
this.value = $.trim(this.value);
var url = $(this).val();
checkQRAddress(url);

View File

@@ -8,7 +8,7 @@
<meta charset="utf-8">
<title>Tautulli - ${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">

View File

@@ -1,8 +1,8 @@
% if newsletter:
<%!
import json
from plexpy import notifiers
from plexpy.helpers import anon_url, checked
from jellypy import notifiers
from jellypy.helpers import anon_url, checked
all_notifiers = sorted(notifiers.get_notifiers(), key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id']))
email_notifiers = [n for n in all_notifiers if n['agent_name'] == 'email']
@@ -50,7 +50,7 @@
</div>
<p class="help-block">
<span id="simple_cron_message">Set the schedule for the newsletter.</span>
<span id="custom_cron_message">Set the schedule for the newsletter using a <a href="${anon_url('https://crontab.guru')}" target="_blank">custom crontab</a>. Only standard cron values are valid.</span>
<span id="custom_cron_message">Set the schedule for the newsletter using a <a href="${anon_url('https://crontab.guru')}" target="_blank" rel="noreferrer">custom crontab</a>. Only standard cron values are valid.</span>
</p>
</div>
<div class="form-group">

View File

@@ -8,9 +8,9 @@
<meta charset="utf-8">
<title>Tautulli - ${title} | ${server_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<style>
* {
@@ -31,7 +31,7 @@
</div>
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
<script>
$(document).ready(function () {
var frame = $('<iframe></iframe>', {

View File

@@ -9,7 +9,7 @@ Version: 0.1
DOCUMENTATION :: END
</%doc>
<% from plexpy import newsletter_handler %>
<% from jellypy import newsletter_handler %>
<ul class="stacked-configs list-unstyled">
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
<li class="newsletter-agent pointer" data-id="${newsletter['id']}">

View File

@@ -1,8 +1,8 @@
% if notifier:
<%
import json
from plexpy import notifiers, users
from plexpy.helpers import checked
from jellypy import notifiers, users
from jellypy.helpers import checked
available_notification_actions = notifiers.available_notification_actions(agent_id=notifier['agent_id'])
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
@@ -49,7 +49,16 @@
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
% if notifier['agent_name'] == 'scripts' and item['name'] == 'scripts_script_folder':
<div class="input-group">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="${item['name']}_browse" data-toggle="browse" data-filter=".folderonly" data-target="#${item['name']}">Browse</button>
</span>
</div>
% else:
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
% endif
</div>
</div>
<p class="help-block">${item['description'] | n}</p>

View File

@@ -1,7 +1,7 @@
<%
import datetime
import plexpy
from plexpy import activity_handler, helpers
import jellypy
from jellypy import activity_handler, helpers
if queue == 'active sessions':
filter_key = 'session_key-'

View File

@@ -32,7 +32,7 @@ DOCUMENTATION :: END
% if data != None:
<%
from plexpy.helpers import page
from jellypy.helpers import cast_to_int, page
%>
% if data:
<div class="dashboard-recent-media-row">
@@ -87,7 +87,7 @@ DOCUMENTATION :: END
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3>
<h3 class="text-muted">
${item['child_count']} Seasons
${item['child_count']} Season${'s' if cast_to_int(item['child_count']) > 1 else ''}
</h3>
<h3 class="text-muted">&nbsp;</h3>
</div>
@@ -151,7 +151,7 @@ DOCUMENTATION :: END
<a href="${page('info', item['rating_key'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
</h3>
</div>
% elif item['media_type'] == 'album':
% elif item['media_type'] == 'album':
<a href="${page('info', item['rating_key'])}" title="${item['parent_title']}">
<div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
@@ -177,7 +177,7 @@ DOCUMENTATION :: END
</h3>
<h3 class="text-muted">&nbsp;</h3>
</div>
% endif
% endif
</li>
</div>
% endfor

View File

@@ -11,10 +11,10 @@ DOCUMENTATION :: END
<%!
import datetime
import plexpy
from plexpy import common, helpers
import jellypy
from jellypy import common, helpers
scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
scheduled_jobs = [j.id for j in jellypy.SCHED.get_jobs()]
%>
<table class="config-scheduler-table small-muted">
@@ -28,7 +28,7 @@ DOCUMENTATION :: END
</tr>
</thead>
<tbody>
% for job in common.SCHEDULER_LIST:
% for job, job_type in common.SCHEDULER_LIST.items():
% if job in scheduled_jobs:
<%
sched_job = plexpy.SCHED.get_job(job)
@@ -41,12 +41,12 @@ DOCUMENTATION :: END
<td>${helpers.format_timedelta_Hms(sched_job.next_run_time - now)}</td>
<td>${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')}</td>
</tr>
% elif job in ('Check for server response', 'Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
% elif job_type == 'websocket' and plexpy.WS_CONNECTED:
<tr>
% if job == 'Check for active sessions':
<td><a class="queue-modal-link" href="#" data-queue="active sessions">${job}</a></td>
<td><a class="queue-modal-link no-highlight" href="#" data-queue="active sessions">${job}</a></td>
% elif job == 'Check for recently added items':
<td><a class="queue-modal-link" href="#" data-queue="recently added">${job}</a></td>
<td><a class="queue-modal-link no-highlight" href="#" data-queue="recently added">${job}</a></td>
% else:
<td>${job}</td>
% endif

View File

@@ -3,12 +3,12 @@
import os
import sys
import plexpy
from plexpy import common, notifiers, newsletters
from plexpy.helpers import anon_url, checked
import jellypy
from jellypy import common, notifiers, newsletters
from jellypy.helpers import anon_url, checked
docker_setting = 'disabled' if plexpy.DOCKER else ''
docker_msg = '<span class="setting-message small">(Controlled by Docker Container)</span>' if plexpy.DOCKER else ''
docker_setting = 'disabled' if jellypy.DOCKER else ''
docker_msg = '<span class="setting-message small">(Controlled by Docker Container)</span>' if jellypy.DOCKER else ''
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'].lower())
available_newsletter_agents = sorted(newsletters.available_newsletter_agents(), key=lambda k: k['label'].lower())
@@ -17,8 +17,6 @@
</%def>
<%def name="headerIncludes()">
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
<link href="${http_root}css/selectize.min.css" rel="stylesheet" />
</%def>
<%def name="body()">
@@ -48,17 +46,17 @@
<!-- Nav tabs -->
<div class="col-md-3">
<ul class="nav-settings list-unstyled" role="tablist">
<li role="presentation" class="active"><a href="#tabs-help_info" aria-controls="tabs-help_info" role="tab" data-toggle="tab">Help & Info</a></li>
<li role="presentation"><a href="#tabs-general" aria-controls="tabs-general" role="tab" data-toggle="tab">General</a></li>
<li role="presentation"><a href="#tabs-homepage" aria-controls="tabs-homepage" role="tab" data-toggle="tab">Homepage</a></li>
<li role="presentation"><a href="#tabs-web_interface" aria-controls="tabs-web_interface" role="tab" data-toggle="tab">Web Interface</a></li>
<li role="presentation"><a href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</a></li>
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
<li role="presentation"><a href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
<li role="presentation"><a href="#tabs-3rd_party_apis" aria-controls="tabs-3rd_party_apis" role="tab" data-toggle="tab">3rd Party APIs</a></li>
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
<li role="presentation" class="active"><a id="nav-help_info" href="#tabs-help_info" aria-controls="tabs-help_info" role="tab" data-toggle="tab">Help & Info</a></li>
<li role="presentation"><a id="nav-tabs-general" href="#tabs-general" aria-controls="tabs-general" role="tab" data-toggle="tab">General</a></li>
<li role="presentation"><a id="nav-tabs-homepage" href="#tabs-homepage" aria-controls="tabs-homepage" role="tab" data-toggle="tab">Homepage</a></li>
<li role="presentation"><a id="nav-tabs-web_interface" href="#tabs-web_interface" aria-controls="tabs-web_interface" role="tab" data-toggle="tab">Web Interface</a></li>
<li role="presentation"><a id="nav-tabs-plex_media_server" href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
<li role="presentation"><a id="nav-tabs-notifications" href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</a></li>
<li role="presentation"><a id="nav-tabs-notification_agents" href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
<li role="presentation"><a id="nav-tabs-newsletter_agents" href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
<li role="presentation"><a id="nav-tabs-3rd_party_apis" href="#tabs-3rd_party_apis" aria-controls="tabs-3rd_party_apis" role="tab" data-toggle="tab">3rd Party APIs</a></li>
<li role="presentation"><a id="nav-tabs-import_backups" href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
<li role="presentation"><a id="nav-tabs-android_app" href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
</ul>
</div>
<div class="col-md-9">
@@ -222,7 +220,7 @@
<p class="help-block">Check for Tautulli updates periodically.</p>
</div>
<div id="git_update_options">
% if not plexpy.FROZEN:
% if not plexpy.SNAP and not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
<div class="checkbox">
<label>
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
@@ -568,29 +566,44 @@
<div class="form-group advanced-setting">
<label for="https_cert">HTTPS Certificate</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_cert_browse" data-toggle="browse" data-filter=".pem" data-target="#https_cert">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL certificate.</p>
<p class="help-block">The location of the SSL certificate in PEM format.</p>
</div>
<div class="form-group advanced-setting">
<label for="https_cert_chain">HTTPS Certificate Chain</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert_chain" name="https_cert_chain" value="${config['https_cert_chain']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_cert_chain" name="https_cert_chain" value="${config['https_cert_chain']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_cert_chain_browse" data-toggle="browse" data-filter=".pem" data-target="#https_cert_chain">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL certificate chain.</p>
<p class="help-block">The location of the SSL certificate chain in PEM format.</p>
</div>
<div class="form-group advanced-setting">
<label for="https_key">HTTPS Key</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_key_browse" data-toggle="browse" data-filter=".pem" data-target="#https_key">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL key.</p>
<p class="help-block">The location of the SSL key in PEM format.</p>
</div>
</div>
@@ -775,7 +788,6 @@
<label>
<input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
</label>
<span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
</div>
<div class="form-group advanced-setting">
@@ -801,17 +813,22 @@
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
<div class="form-group advanced-setting">
<label for="pms_logs_folder">Logs Folder</label>
<label for="pms_logs_folder">Plex Logs Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^[^\~\%]" data-parsley-errors-container="#pms_logs_folder_error" data-parsley-error-message="Shortcuts are not recognized.">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^[^\~\%]" data-parsley-errors-container="#pms_logs_folder_error" data-parsley-error-message="Shortcuts are not recognized.">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="pms_logs_folder_browse" data-toggle="browse" data-filter=".folderonly" data-target="#pms_logs_folder">Browse</button>
</span>
</div>
</div>
<div id="pms_logs_folder_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">
Optional: Set your Plex logs folder to use Tautulli as a log viewer. Plex logs are not needed for Tautulli to function.
A complete folder path is required, shortcuts are not recognized, and the logs must be accessible from the machine where Tautulli is installed.
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank">Click here</a> for help.
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank" rel="noreferrer">Click here</a> for help.
</p>
</div>
<div class="checkbox advanced-setting">
@@ -832,7 +849,6 @@
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
</label>
<span id="cloudMonitorUpdates" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
</div>
<div id="pms_update_options">
@@ -866,36 +882,6 @@
</p>
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
</label>
<span id="cloudMonitorRemoteAccess" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<span id="remoteAccessCheck" class="settings-warning"></span>
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div>
<div id="monitor_remote_access_options">
<div class="form-group advanced-setting">
<label for="remote_access_ping_interval">Remote Access Ping Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="remote_access_ping_interval" name="remote_access_ping_interval" value="${config['remote_access_ping_interval']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#remote_access_ping_interval_error" required>
</div>
<div id="remote_access_ping_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in seconds) Tautulli will ping the Plex Media Server for the remote access status. Minimum 60.</p>
</div>
<div class="form-group advanced-setting">
<label for="remote_access_ping_threshold">Remote Access Ping Threshold</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="remote_access_ping_threshold" name="remote_access_ping_threshold" value="${config['remote_access_ping_threshold']}" size="5" data-parsley-min="1" data-parsley-trigger="change" data-parsley-errors-container="#remote_access_ping_threshold_error" required>
</div>
<div id="remote_access_ping_threshold_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The number of consecutive remote access status failures to consider remote access as down. Minimum 1.</p>
</div>
</div>
<div class="form-group advanced-setting">
<label for="refresh_users_interval">Users List Refresh Interval</label>
@@ -991,15 +977,21 @@
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Consecutive Notifications
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Playback Stop Notifications Exceeding Watched Percent
</label>
<p class="help-block">Enable to allow sending of consecutive notifications (i.e. both watched &amp; stopped notifications).</p>
<p class="help-block">
Enable to allow sending of playback stop notifications after the watched percent is exceeded.
Disable to only send playback stop notifications below the watched percent.
</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" name="notify_concurrent_by_ip" id="notify_concurrent_by_ip" value="1" ${config['notify_concurrent_by_ip']}> User Concurrent Streams Notifications by IP Address
</label>
<p class="help-block">Enable to only send a notification of concurrent streams by a single user from different IP addresses.</p>
<p class="help-block">
Enable to only send a concurrent streams notification by a single user from different IP addresses.
Disable to send a concurrent streams notification anytime the concurrent stream threshold is exceeded regardless of IP address.
</p>
</div>
<div class="form-group">
<label for="notify_concurrent_threshold">User Concurrent Stream Threshold</label>
@@ -1011,6 +1003,15 @@
</div>
<p class="help-block">The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" name="notify_new_device_initial_only" id="notify_new_device_initial_only" value="1" ${config['notify_new_device_initial_only']}> User New Device Notification First Time Only
</label>
<p class="help-block">
Enable to only send a new device notification the first time a user streams from a new device.
Disable to send a new device notification everytime a user streams from the device until it is recorded in history (i.e. exceeds the ignore interval).
</p>
</div>
<div class="form-group advanced-setting">
<label for="notify_concurrent_threshold">Continued Session Threshold</label>
<div class="row">
@@ -1049,7 +1050,7 @@
</p>
</div>
<div class="form-group">
<label for="notify_recently_added_delay">Notification Delay</label>
<label for="notify_recently_added_delay">Recently Added Notification Delay</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_recently_added_delay" name="notify_recently_added_delay" value="${config['notify_recently_added_delay']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_recently_added_delay_error" required>
@@ -1082,6 +1083,21 @@
</p>
</div>-->
<div class="padded-header">
<h3>Remote Access Notifications</h3>
</div>
<div class="form-group">
<label for="notify_remote_access_threshold">Remote Access Down Threshold</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_remote_access_threshold" name="notify_remote_access_threshold" value="${config['notify_remote_access_threshold']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_remote_access_threshold_error" required>
</div>
<div id="notify_remote_access_threshold_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The duration (in seconds) for Plex remote access to be down before sending a notification. Minimum 60, default 60.</p>
</div>
<div class="padded-header">
<h3>Newsletters</h3>
</div>
@@ -1134,10 +1150,15 @@
</p>
</div>
<div class="form-group advanced-setting">
<label for="newsletter_dir">Custom Newsletter Templates Folder</label>
<label for="newsletter_custom_dir">Custom Newsletter Templates Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="newsletter_custom_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#newsletter_custom_dir">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">Optional: Enter the full path to your custom newsletter templates folder. Leave blank for default.</p>
@@ -1145,8 +1166,13 @@
<div class="form-group advanced-setting">
<label for="newsletter_dir">Newsletter Output Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="newsletter_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#newsletter_dir" ${docker_setting}>Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
@@ -1166,7 +1192,7 @@
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
</p>
<p class="help-block">
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>Notification Agents Guide</a> for instructions on setting up each notification agent.
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">Notification Agents Guide</a> for instructions on setting up each notification agent.
</p>
<br />
<div id="plexpy-notifiers-table">
@@ -1228,7 +1254,7 @@
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
<div class="form-group">
<p class="help-block">
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up Imgur.<br>
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">3rd Party APIs Guide</a> for instructions on setting up Imgur.<br>
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
</p>
</div>
@@ -1251,7 +1277,7 @@
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
<div class="form-group">
<p class="help-block">
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up Cloudinary.
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">3rd Party APIs Guide</a> for instructions on setting up Cloudinary.
</p>
</div>
<div class="form-group">
@@ -1334,14 +1360,32 @@
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
<div class="padded-header">
<h3>Database Import</h3>
<h3>Import</h3>
</div>
<p class="help-block">Click a button below to import an existing database from the selected app.</p>
<div class="btn-group">
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="tautulli">Tautulli</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button>
<div class="form-group">
<label for="database_import">Database Import</label>
<p class="help-block">Click a button below to import an existing database from the selected app.</p>
<div class="row">
<div class="col-md-9">
<div class="btn-group">
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="tautulli">Tautulli</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="config_import">Configuration Import</label>
<p class="help-block">Click the button below to import a previous Tautulli configuration.</p>
<div class="row">
<div class="col-md-9">
<div class="btn-group">
<button class="btn btn-form toggle-config-import-modal" type="button" data-target="#config-import-modal" data-toggle="modal">Tautulli</button>
</div>
</div>
</div>
</div>
<div class="padded-header">
@@ -1379,8 +1423,13 @@
<div class="form-group">
<label for="log_dir">Log Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="log_dir" name="log_dir" value="${config['log_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="log_dir" name="log_dir" value="${config['log_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="log_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#log_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_logs">Clear Logs</button>
</div>
@@ -1390,8 +1439,13 @@
<div class="form-group">
<label for="backup_dir">Backup Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="backup_dir" name="backup_dir" value="${config['backup_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="backup_dir" name="backup_dir" value="${config['backup_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="backup_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#backup_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="backup_config">Backup Config</button>
<button class="btn btn-form" type="button" id="backup_database">Backup Database</button>
@@ -1402,8 +1456,13 @@
<div class="form-group">
<label for="cache_dir">Cache Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="cache_dir" name="cache_dir" value="${config['cache_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="cache_dir" name="cache_dir" value="${config['cache_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="cache_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#cache_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_cache">Clear All Cache</button>
<button class="btn btn-form" type="button" id="clear_image_cache">Clear Image Cache</button>
@@ -1411,6 +1470,22 @@
</div>
</div>
</div>
<div class="form-group">
<label for="export_dir">Export Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="export_dir" name="export_dir" value="${config['export_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="export_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#export_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_exports">Clear All Exports</button>
</div>
</div>
</div>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
@@ -1425,9 +1500,9 @@
<div class="form-group">
<label>Get the App</label>
<p class="help-block">
Get the <a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank">Tautulli Remote</a> app on Google Play<sup>TM</sup> to access Tautulli from your Android device!<br />
Get the <a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank" rel="noreferrer">Tautulli Remote</a> app on Google Play<sup>TM</sup> to access Tautulli from your Android device!<br />
<span class="google-play-badge">
<a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank"><img alt="Get it on Google Play" src="images/en-play-badge.png" /></a>
<a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank" rel="noreferrer"><img alt="Get it on Google Play" src="images/en-play-badge.png" /></a>
</span>
</p>
</div>
@@ -1463,14 +1538,14 @@
</div>
<div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Issues/blob/master/README.md' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">guidelines</a>
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Issues/blob/master/README.md' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">guidelines</a>
in the README document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
<br /><br />
Your post may be removed for failure to follow the guidelines.
</div>
</div>
<div class="modal-footer">
<a href="#" target="_blank" id="guidelines-continue" class="btn btn-bright">Continue</a>
<a href="#" target="_blank" rel="noreferrer" id="guidelines-continue" class="btn btn-bright">Continue</a>
</div>
</div>
</div>
@@ -1485,12 +1560,12 @@
</div>
<div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">FAQ</a>
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">FAQ</a>
before asking for help!</strong>
</div>
</div>
<div class="modal-footer">
<a href="#" target="_blank" id="support-continue" class="btn btn-bright">Continue</a>
<a href="#" target="_blank" rel="noreferrer" id="support-continue" class="btn btn-bright">Continue</a>
</div>
</div>
</div>
@@ -1532,6 +1607,7 @@
</div>
</div>
<div id="app-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="app-import-modal"></div>
<div id="config-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="config-import-modal"></div>
<div id="add-notifier-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="add-notifier-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
@@ -1886,7 +1962,7 @@ Rating: {rating}/10 --> Rating: /10
</p>
<p class="help-block" id="api_qr_private" style="display: none;">
Note: This is a private IP address. Tautulli will not be reachable outside of your home network.
Access Tautulli via an externally address or manually enter the address above to generate the QR code for remote access.
Access Tautulli via an external address or manually enter the address above to generate the QR code for remote access.
</p>
<p class="help-block" id="api_qr_https" style="display: none;">
Note: This URL is not secure. Requests between the app and the server will not be encrypted.
@@ -1939,8 +2015,7 @@ Rating: {rating}/10 --> Rating: /10
<%def name="javascriptIncludes()">
<script src="${http_root}js/parsley.min.js"></script>
<script src="${http_root}js/Sortable.min.js"></script>
<script src="${http_root}js/selectize.min.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.inputaffix.min.js"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
<script>
function getConfigurationTable() {
@@ -1949,7 +2024,7 @@ Rating: {rating}/10 --> Rating: /10
cache: false,
async: true,
complete: function(xhr, status) {
$("#plexpy-configuration-table").html(xhr.responseText);
$("#jellypy-configuration-table").html(xhr.responseText);
}
});
}
@@ -1960,7 +2035,7 @@ Rating: {rating}/10 --> Rating: /10
cache: false,
async: true,
complete: function(xhr, status) {
$("#plexpy-scheduler-table").html(xhr.responseText);
$("#jellypy-scheduler-table").html(xhr.responseText);
}
});
}
@@ -1971,7 +2046,7 @@ Rating: {rating}/10 --> Rating: /10
cache: false,
async: true,
complete: function(xhr, status) {
$("#plexpy-notifiers-table").html(xhr.responseText);
$("#jellypy-notifiers-table").html(xhr.responseText);
}
});
}
@@ -1996,7 +2071,7 @@ Rating: {rating}/10 --> Rating: /10
cache: false,
async: true,
complete: function(xhr, status) {
$("#plexpy-newsletters-table").html(xhr.responseText);
$("#jellypy-newsletters-table").html(xhr.responseText);
}
});
}
@@ -2021,7 +2096,7 @@ Rating: {rating}/10 --> Rating: /10
cache: false,
async: true,
complete: function(xhr, status) {
$("#plexpy-mobile-devices-table").html(xhr.responseText);
$("#jellypy-mobile-devices-table").html(xhr.responseText);
}
});
}
@@ -2040,6 +2115,22 @@ Rating: {rating}/10 --> Rating: /10
});
}
$("#browse-path-modal").on('hidden.bs.modal', function() {
$("#select-browse-file").unbind('click');
});
function openBrowsePath(key, path, filter_ext, file_description, select_target) {
$("#browse-path-type").text(file_description);
$("#browse-path-modal").modal('show');
$("#select-browse-file").click(function () {
$("#browse-path-modal").modal('hide');
$(select_target).val($("#browse-path").val()).change();
});
browsePath(key, path, filter_ext);
}
function browsePath(key, path, filter_ext) {
$("#browse-path-status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
getBrowsePath(key, path, filter_ext).then(function (data) {
@@ -2076,9 +2167,9 @@ $(document).ready(function() {
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
var prefix = "tabs_";
if (hash) {
$('.nav-settings a[href='+hash.replace(prefix,"")+']').tab('show');
$('.nav-settings #nav-' + hash.replace('#' + prefix, "")).tab('show');
}
// Change hash for page-reload
@@ -2103,6 +2194,7 @@ $(document).ready(function() {
function preSaveChecks(_callback) {
verifyPMSWebURL();
setBaseURLSuffix(true);
if (serverChanged) {
verifyServer(_callback);
} else if (typeof _callback === "function") {
@@ -2122,6 +2214,7 @@ $(document).ready(function() {
getNewslettersTable();
getMobileDevicesTable();
loadUpdateDistros();
setBaseURLSuffix();
settingsChanged = false;
}
@@ -2158,7 +2251,6 @@ $(document).ready(function() {
initConfigCheckbox('#https_create_cert');
initConfigCheckbox('#check_github');
initConfigCheckbox('#monitor_pms_updates');
initConfigCheckbox('#monitor_remote_access');
initConfigCheckbox('#newsletter_self_hosted');
$('#menu_link_shutdown').click(function() {
@@ -2245,6 +2337,12 @@ $(document).ready(function() {
confirmAjaxCall(url, msg);
});
$("#clear_exports").click(function () {
var msg = 'Are you sure you want to clear the Tautulli metadata exports?';
var url = 'delete_export?delete_all=true';
confirmAjaxCall(url, msg);
});
$("#clear_logs").click(function () {
var msg = 'Are you sure you want to clear the Tautulli logs?';
var url = 'delete_logs';
@@ -2404,7 +2502,6 @@ $(document).ready(function() {
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
$('#pms_url_manual').prop('checked', false);
$('#pms_url').val('Please verify your server above to retrieve the URL');
PMSCloudCheck();
},
onDropdownOpen: function() {
this.clear();
@@ -2435,38 +2532,6 @@ $(document).ready(function() {
}
getServerOptions();
function PMSCloudCheck() {
if ($('#pms_is_cloud').val() === "1") {
$('#pms_port').val(443).prop('readonly', true);
$('#pms_is_remote_checkbox').prop('checked', true).prop('disabled', true);
$('#pms_is_remote').val(1);
$('#pms_ssl_checkbox').prop('checked', true).prop('disabled', true);
$('#pms_ssl').val(1);
$('#pms_url_manual').prop('checked', false).prop('disabled', true);
$('#monitor_pms_updates').prop('checked', false).prop('disabled', true);
$('#pms_update_options').hide();
$('#monitor_remote_access').prop('checked', false).prop('disabled', true);
$('#cloudManualConnection').show();
$('#cloudMonitorUpdates').show();
$('#cloudMonitorRemoteAccess').show();
$('#remoteAccessCheck').hide();
} else {
$('#pms_port').prop('readonly', false);
$('#pms_is_remote_checkbox').prop('disabled', false);
$('#pms_is_remote').val($('#pms_is_remote_checkbox').is(':checked') ? 1 : 0);
$('#pms_ssl_checkbox').prop('disabled', false);
$('#pms_ssl').val($('#pms_ssl_checkbox').is(':checked') ? 1 : 0);
$('#pms_url_manual').prop('disabled', false);
$('#monitor_pms_updates').prop('disabled', false);
$('#monitor_remote_access').prop('disabled', false);
$('#cloudManualConnection').hide();
$('#cloudMonitorUpdates').hide();
$('#cloudMonitorRemoteAccess').hide();
remoteAccessEnabledCheck()
}
}
PMSCloudCheck();
function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val();
var pms_port = $("#pms_port").val();
@@ -2579,25 +2644,22 @@ $(document).ready(function() {
});
});
// Load config import modal
$(".toggle-config-import-modal").click(function() {
$.ajax({
url: 'import_config_tool',
cache: false,
async: true,
complete: function(xhr, status) {
$("#config-import-modal").html(xhr.responseText);
}
});
});
pms_version = false;
pms_logs_debug = false;
pms_logs = false;
function remoteAccessEnabledCheck() {
$.ajax({
url: 'get_server_pref',
data: { pref: 'PublishServerOnPlexOnlineKey' },
async: true,
success: function(data) {
if (data === 'false' || data === '0') {
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
}
}
});
}
remoteAccessEnabledCheck();
// Sortable home_sections
function set_home_sections() {
var home_sections = [];
@@ -2864,10 +2926,19 @@ $(document).ready(function() {
});
});
$('#http_base_url').change(function () {
$(this).val($(this).val().replace(/\/*$/, ''));
$('#http_root').change(function() {
setBaseURLSuffix();
});
function setBaseURLSuffix(clear) {
if (clear){
$('#http_base_url').suffix("");
} else {
$('#http_base_url').suffix($('#http_root').val());
}
}
setBaseURLSuffix();
function apiEnabled() {
var api_enabled = $('#api_enabled').prop('checked');
$('#app_api_msg').toggle(!(api_enabled));
@@ -2986,7 +3057,7 @@ $(document).ready(function() {
});
function gotoSetting(tab, setting){
$("a[href=#tabs-" + tab + "]").click();
$("#nav-tabs-" + tab).click();
if (setting) {
_setting = '#' + setting;
if ($(_setting).closest('.advanced-setting').length && !$('#menu_link_show_advanced_settings').hasClass('active')) {
@@ -3034,7 +3105,7 @@ $(document).ready(function() {
if (news_item.subtitle) { content.append(subtitle); }
content.append(body);
var li = $('<li/>').append(header).append(content)
if (index === 0 && Math.abs(now.diff(date, 'days')) < 7) {
if (index === 0 && Math.abs(now.diff(date, 'days')) <= 30) {
li.addClass('open');
content.css('display', 'block');
}
@@ -3050,6 +3121,14 @@ $(document).ready(function() {
tautulli_news.html('<p class="help-block"><i class="fa fa-exclamation-triangle"></i>&nbsp; Failed to retrieve news.</p>')
}
});
$("body").on('click', '[data-toggle=browse]', function () {
var filter = $(this).data('filter');
var target = $(this).data('target');
var path = $(target).val();
var description = $(this).data('description') || $("label[for='" + target.replace('#', '') + "']").text();
openBrowsePath(null, path, filter, description, target);
});
});
</script>
</%def>

View File

@@ -22,10 +22,10 @@
<div class="modal-body" id="modal-text">
<div align="center">
% if message == "Shutting Down":
<h3><i class="fa fa-refresh fa-spin"></i> Tautulli is ${message}.</h3>
<h3><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is ${message.lower()}</h3>
<br />
% else:
<h3><i class="fa fa-refresh fa-spin"></i> Tautulli is ${message}.</h3>
<h3><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is ${message.lower()}</h3>
<br />
<h4>Restart in <span class="countdown"></span></h4>
% endif

View File

@@ -39,7 +39,7 @@ DOCUMENTATION :: END
% if data:
<%
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
from jellypy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
%>
<div class="modal-dialog" role="document">
<div class="modal-content">

View File

@@ -1,6 +1,6 @@
<%inherit file="base.html"/>
<%!
from plexpy.helpers import anon_url
from jellypy.helpers import anon_url
%>
<%def name="headIncludes()">
</%def>
@@ -16,10 +16,10 @@
</div>
<div class="button-bar">
<div class="btn-group">
<a class="btn btn-dark" href="${anon_url('https://tautulli.com/discord')}" target="_blank"><i class="fab fa-discord"></i>&nbsp; Join Discord</a>
<a class="btn btn-dark" href="${anon_url('https://tautulli.com/discord')}" target="_blank" rel="noreferrer"><i class="fab fa-discord"></i>&nbsp; Join Discord</a>
</div>
<div class="btn-group">
<a class="btn btn-dark" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank"><i class="fab fa-reddit"></i>&nbsp; Join Reddit</a>
<a class="btn btn-dark" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank" rel="noreferrer"><i class="fab fa-reddit"></i>&nbsp; Join Reddit</a>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="popout-iframe-button"><i class="fa fa-external-link"></i> Pop Out Chat</button>

View File

@@ -171,6 +171,7 @@ DOCUMENTATION :: END
</p>
<p> with </p>
<p><span id="new_title"></span></p>
<p>from the <strong><span id="new_library"></span></strong> library?</p>
% if query['media_type'] != 'movie':
<p>All items for <strong>${query['grandparent_title']}</strong> will also be updated.</p>
% endif
@@ -211,10 +212,12 @@ DOCUMENTATION :: END
$(document).on('click', '#search-results-list a', function (e) {
e.preventDefault();
var new_rating_key = $(this).attr('id');
var new_rating_key = $(this).data('rating_key');
var new_library_section = $(this).data('library_name');
var new_href = $(this).attr('href');
$('#new_title').html($(this).find('.item-children-instance-text-wrapper').html());
$('#new_library').text(new_library_section);
$('#confirm-modal-update').modal();
$('#confirm-modal-update').one('click', '#confirm-update', function () {

View File

@@ -26,7 +26,7 @@ DOCUMENTATION :: END
<%inherit file="base.html"/>
<%!
from plexpy import helpers
from jellypy import helpers
%>
<%def name="headIncludes()">
@@ -43,7 +43,9 @@ DOCUMENTATION :: END
<div class="summary-navbar">
<div class="col-md-12">
<div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb"></ul>
<ul class="list-unstyled breadcrumb">
<li class="active">${data['friendly_name']}</li>
</ul>
</div>
</div>
</div>
@@ -67,12 +69,16 @@ DOCUMENTATION :: END
% endif
</div>
<div class="user-info-nav">
<ul class="user-info-nav" role="tablist">
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
<li><a id="sync-tab-btn" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
<li><a id="ip-tab-btn" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
<li><a id="login-tab-btn" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
<ul class="nav nav-list nav-pills" role="tablist">
<li class="active"><a id="nav-tabs-profile" href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
<li><a id="nav-tabs-playlists" href="#tabs-playlists" role="tab" data-toggle="tab">Playlists</a></li>
% if _session['user_group'] == 'admin':
<li><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
% endif
<li><a id="nav-tabs-synceditems" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
<li><a id="nav-tabs-ipaddresses" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
<li><a id="nav-tabs-tautullilogins" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
</ul>
</div>
</div>
@@ -210,6 +216,99 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-playlists">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-list-alt"></i> Playlists for <strong>
<span class="set-username">${data['friendly_name']}</span>
</strong>
</span>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
<div class="btn-group">
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
data-user_id="${data['user_id']}" data-media_type="playlist" data-sub_media_type="video,audio,photo"
data-export_type="playlist">
<i class="fa fa-file-export"></i> Export playlists
</button>
</div>
% endif
<div class="btn-group">
<button class="btn btn-dark refresh-playlists-table-button" id="refresh-playlists-table">
<i class="fa fa-refresh"></i> Refresh playlists
</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-playlists"></div>
</div>
</div>
<div class="table-card-back">
<table class="display playlists_table" id="playlists_table-SID-${data['user_id']}" width="100%">
<thead>
<tr>
<th align="left" id="playlistTitle">Playlist Title</th>
<th align="left" id="playlistLeafCount">Playlist Items</th>
<th align="left" id="playlistDuration">Playlist Duration</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
% if _session['user_group'] == 'admin':
<div role="tabpanel" class="tab-pane" id="tabs-export">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-file-export"></i> Metadata Exports for <strong>
<span class="set-username">${data['friendly_name']}</span>
</strong>
</span>
</div>
<div class="button-bar">
<div class="btn-group">
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
<i class="fa fa-refresh"></i> Refresh exports
</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
</div>
</div>
<div class="table-card-back">
<table class="display export_table" id="export_table-SID-${data['user_id']}" width="100%">
<thead>
<tr>
<th align="left" id="timestamp">Exported At</th>
<th align="left" id="media_type_title">Media Type</th>
<th align="left" id="rating_key">Rating Key</th>
<th align="left" id="filename">Filename</th>
<th align="left" id="file_format">File Format</th>
<th align="left" id="metadata_level">Metadata Level</th>
<th align="left" id="media_info_level">Media Info Level</th>
<th align="left" id="media_info_level">Custom Fields</th>
<th align="left" id="file_size">File Size</th>
<th align="left" id="complete">Download</th>
<th align="left" id="delete">Delete</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
% endif
<div role="tabpanel" class="tab-pane" id="tabs-synceditems">
<div class="container-fluid">
<div class="row">
@@ -278,13 +377,15 @@ DOCUMENTATION :: END
<div class="btn-group">
<button class="btn btn-dark refresh-ip-address-button" id="refresh-ip-address-list"><i class="fa fa-refresh"></i> Refresh IP addresses</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-ip-address"></div>
</div>
</div>
<div class="table-card-back">
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
<thead>
<tr>
<th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_seen">Last Streamed</th>
<th align="left" id="first_seen">First Streamed</th>
<th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Last Platform</th>
<th align="left" id="player">Last Player</th>
@@ -392,6 +493,8 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
</div>
</%def>
<%def name="javascriptIncludes()">
@@ -409,8 +512,9 @@ DOCUMENTATION :: END
var username = '${data['username'].replace("'", "\\'")}';
</script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script src="${http_root}js/tables/playlists_table.js${cache_param}"></script>
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
<script src="${http_root}js/tables/user_ips.js${cache_param}"></script>
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
<script src="${http_root}js/tables/login_logs.js${cache_param}"></script>
@@ -419,6 +523,8 @@ DOCUMENTATION :: END
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
$(".inactive-user-tooltip").tooltip();
function loadHistoryTable(media_type) {
// Build watch history table
history_table_options.ajax = {
@@ -450,6 +556,49 @@ DOCUMENTATION :: END
});
}
$('#nav-tabs-history').on('shown.bs.tab', function() {
if (typeof(history_table) === 'undefined') {
var media_type = getLocalStorage('user_' + user_id + '-history_media_type', 'all');
$('#history-' + media_type).prop('checked', true);
$('#history-' + media_type).closest('label').addClass('active');
loadHistoryTable(media_type);
}
});
$("#refresh-history-list").click(function () {
history_table.draw();
});
function loadPlaylistsTable() {
// Build playlists table
playlists_table_options.ajax = {
url: 'get_playlists_list',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
user_id: user_id
};
}
};
playlists_table = $('#playlists_table-SID-${data["user_id"]}').DataTable(playlists_table_options);
var colvis = new $.fn.dataTable.ColVis(playlists_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-playlists');
clearSearchButton('playlists_table-SID-${data["user_id"]}', playlists_table);
}
$('#nav-tabs-playlists').on('shown.bs.tab', function() {
if (typeof(playlists_table) === 'undefined') {
loadPlaylistsTable();
}
});
$("#refresh-playlists-table").click(function () {
playlists_table.draw();
});
function loadSyncTable() {
// Build user sync table
sync_table_options.ajax = {
@@ -465,6 +614,16 @@ DOCUMENTATION :: END
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
}
$('#nav-tabs-synceditems').on('shown.bs.tab', function() {
if (typeof(sync_table) === 'undefined') {
loadSyncTable(user_id);
}
});
$("#refresh-syncs-list").click(function() {
sync_table.ajax.reload();
});
function loadIPAddressTable() {
// Build user IP table
user_ip_table_options.ajax = {
@@ -479,9 +638,22 @@ DOCUMENTATION :: END
};
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
var colvis_user_ip = new $.fn.dataTable.ColVis( user_ip_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark'} );
$( colvis_user_ip.button() ).appendTo('#button-bar-ip-address');
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
}
$('#nav-tabs-ipaddresses').on('shown.bs.tab', function() {
if (typeof(user_ip_table) === 'undefined') {
loadIPAddressTable(user_id);
}
});
$("#refresh-ip-address-list").click(function () {
user_ip_table.draw();
});
function loadLoginTable() {
// Build user login table
login_log_table_options.ajax = {
@@ -503,52 +675,141 @@ DOCUMENTATION :: END
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
}
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
if (typeof(history_table) === 'undefined') {
var media_type = getLocalStorage('user_' + user_id + '-history_media_type', 'all');
$('#history-' + media_type).prop('checked', true);
$('#history-' + media_type).closest('label').addClass('active');
loadHistoryTable(media_type);
}
});
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
if (typeof(sync_table) === 'undefined') {
loadSyncTable(user_id);
}
});
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
if (typeof(user_ip_table) === 'undefined') {
loadIPAddressTable(user_id);
}
});
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
$('#nav-tabs-tautullilogins').on('shown.bs.tab', function() {
if (typeof(login_log_table) === 'undefined') {
loadLoginTable(user_id);
}
});
$("#refresh-history-list").click(function () {
history_table.draw();
});
$("#refresh-syncs-list").click(function() {
sync_table.ajax.reload();
});
$("#refresh-ip-address-list").click(function () {
user_ip_table.draw();
});
$("#refresh-login-list").click(function () {
login_log_table.draw();
});
$(".inactive-user-tooltip").tooltip();
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: {
user_id: user_id,
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() {
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");
}
});
$(document).ready(function () {
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.nav-list #nav-' + hash.replace('#' + prefix, "")).tab('show').trigger('show.bs.tab');
}
// Change hash for page-reload
$('.nav-list a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
// Populate watch time stats
$.ajax({
url: 'user_watch_time_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-time-stats").html(xhr.responseText);
}
});
// Populate platform stats
$.ajax({
url: 'user_player_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-player-stats").html(xhr.responseText);
}
});
});
</script>
% if _session['user_group'] == 'admin':
<script>
function loadExportTable() {
// Build export table
export_table_options.ajax = {
url: 'get_export_list',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
user_id: user_id
};
}
};
export_table = $('#export_table-SID-${data["user_id"]}').DataTable(export_table_options);
export_table.columns([2, 7]).visible(false);
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-export');
clearSearchButton('export_table-SID-${data["user_id"]}', export_table);
}
$('#nav-tabs-export').on('shown.bs.tab', function() {
if (typeof(export_table) === 'undefined') {
loadExportTable();
}
});
$("#refresh-export-table").click(function () {
export_table.draw();
});
% if _session['user_group'] == 'admin':
$("#edit-user-tooltip").tooltip();
// Load edit user modal
@@ -565,6 +826,23 @@ DOCUMENTATION :: END
});
});
$(".export-button").click(function() {
$.ajax({
url: 'export_metadata_modal',
data: {
user_id: $(this).data('user_id'),
media_type: $(this).data('media_type'),
sub_media_type: $(this).data('sub_media_type'),
export_type: $(this).data('export_type')
},
cache: false,
async: true,
complete: function(xhr, status) {
$("#export-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
@@ -643,100 +921,7 @@ DOCUMENTATION :: END
});
}
});
% endif
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: {
user_id: user_id,
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() {
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");
}
});
$(document).ready(function () {
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
}
// Change hash for page-reload
$('.user-info-nav a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
// Populate watch time stats
$.ajax({
url: 'user_watch_time_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-time-stats").html(xhr.responseText);
}
});
// Populate platform stats
$.ajax({
url: 'user_player_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-player-stats").html(xhr.responseText);
}
});
});
</script>
% endif
% endif
</%def>

View File

@@ -28,7 +28,7 @@ DOCUMENTATION :: END
% if data:
<%
from plexpy.helpers import page
from jellypy.helpers import page
%>
<div class="dashboard-recent-media-row">
<div id="recently-watched-row-scroller" style="left: 0;">

View File

@@ -34,7 +34,7 @@
<th align="left" id="edit_row">Edit</th>
<th align="right" id="avatar"></th>
<th align="left" id="friendly_name">User</th>
<th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_seen">Last Streamed</th>
<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>
@@ -82,7 +82,6 @@
<script src="${http_root}js/dataTables.colVis.js"></script>
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/users.js${cache_param}"></script>
<script>
$(document).ready(function () {

View File

@@ -1,6 +1,6 @@
<%
import plexpy
from plexpy import common, helpers
import jellypy
from jellypy import common, helpers
%>
<!doctype html>
@@ -12,7 +12,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
<link href="${http_root}css/bootstrap-wizard.css" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet">
@@ -21,27 +21,27 @@
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
<!-- ICONS -->
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="wizard" id="setup-wizard" data-title="Tautulli Setup Wizard">
<div class="wizard" id="setup-wizard" data-title="JellyPy Setup Wizard">
<form>
<div class="wizard-card" data-cardname="card1">
<div style="float: right;">
@@ -50,10 +50,7 @@
<h3 style="line-height: 50px;">Welcome!</h3>
<div class="wizard-input-section">
<p class="welcome-message">
Thanks for taking the time to try out Tautulli. Hope you find it useful.
</p>
<p class="welcome-message">
Tautulli requires a permanent internet connection to ensure a reliable experience.
Thanks for taking the time to try out JellyPy. Hope you find it useful.
</p>
<p class="welcome-message">
This wizard will help you get set up, to continue press Next.
@@ -65,122 +62,150 @@
<h3>Authentication</h3>
<div class="wizard-input-section">
<p class="help-block">
Please setup an admin username and password for Tautulli.
Please setup an admin username and password for JellyPy.
</p>
</div>
<div class="wizard-input-section">
<label for="http_username">HTTP Username</label>
<label for="http_username">Username</label>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control auth-settings" id="http_username" name="http_username" value="" size="30">
<input type="text" class="form-control auth-settings" id="http_username"
name="http_username" value="" size="30">
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="http_password">HTTP Password</label>
<label for="http_password">Password</label>
<div class="row">
<div class="col-xs-8">
<input type="password" class="form-control auth-settings" id="http_password" name="http_password" value="" size="30" autocomplete="new-password">
<input type="password" class="form-control auth-settings" id="http_password"
name="http_password" value="" size="30" autocomplete="new-password">
</div>
</div>
</div>
<input type="hidden" class="form-control" name="http_hash_password" id="http_hash_password" value="1">
<input type="hidden" class="form-control" name="http_hash_password" id="http_hash_password"
value="1">
<input type="hidden" class="form-control" name="http_plex_admin" id="http_plex_admin" value="1">
<input type="hidden" id="authentication_valid" data-validate="validateAuthentication" value="">
<span style="display: none;" id="authentication-status"></span>
</div>
<div class="wizard-card" data-cardname="card3">
<h3>Plex Account</h3>
<h3>Jellyfin Server</h3>
<div class="wizard-input-section">
<p class="help-block">
Tautulli requires a Plex.tv account. Click the button below to sign in on Plex.tv. You may need to allow popups in your browser.
</p>
</div>
<input type="hidden" class="form-control" name="pms_token" id="pms_token" value="" data-validate="validatePMStoken">
<a class="btn btn-dark" id="sign-in-plex" href="#" role="button">Sign In with Plex</a>
<span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
</div>
<div class="wizard-card" data-cardname="card4">
<h3>Plex Media Server</h3>
<div class="wizard-input-section">
<p class="help-block">
Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.
Select your Jellyfin Server from the dropdown menu or enter an IP address or hostname.
</p>
</div>
<div class="wizard-input-section">
<label for="pms_ip_selectize">Plex IP Address or Hostname</label>
<label for="jellyfin_ip_selectize">Jellyfin IP Address or Hostname</label>
<div class="row">
<div class="col-xs-12">
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip_selectize">
% if config['pms_identifier']:
<option value="${config['pms_ip']}:${config['pms_port']}"
data-identifier="${config['pms_identifier']}"
data-ip="${config['pms_ip']}"
data-port="${config['pms_port']}"
data-local="${int(not int(config['pms_is_remote']))}"
data-ssl="${config['pms_ssl']}"
data-is_cloud="${config['pms_is_cloud']}"
data-label="${config['pms_name'] or 'Local'}"
selected>${config['pms_ip']}</option>
<select class="form-control jellyfin-settings selectize-jellyfin-ip" id="jellyfin_ip_selectize">
% if config['jellyfin_identifier']:
<option value="${config['jellyfin_ip']}:${config['jellyfin_port']}"
data-identifier="${config['jellyfin_identifier']}"
data-ip="${config['jellyfin_ip']}"
data-port="${config['jellyfin_port']}"
data-local="${int(not int(config['jellyfin_is_remote']))}"
data-ssl="${config['jellyfin_ssl']}"
data-is_cloud="${config['jellyfin_is_cloud']}"
data-label="${config['jellyfin_name'] or 'Local'}"
selected>${config['jellyfin_ip']}
</option>
% endif
</select>
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="pms_port">Plex Port</label>
<label for="jellyfin_port">Jellyfin Port</label>
<div class="row">
<div class="col-xs-3">
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
<input type="text" class="form-control jellyfin-settings" name="jellyfin_port" id="jellyfin_port"
placeholder="8096" value="${config['jellyfin_port']}" required>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
<input type="checkbox" id="jellyfin_ssl_checkbox"
class="checkbox-toggle jellyfin-settings" data-id="jellyfin_ssl" value="1"
${helpers.checked(config['jellyfin_ssl'])}> Use SSL
<input type="hidden" id="jellyfin_ssl" name="jellyfin_ssl" value="${config['jellyfin_ssl']}">
</label>
</div>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
<input type="checkbox" id="jellyfin_is_remote_checkbox"
class="checkbox-toggle jellyfin-settings" data-id="jellyfin_is_remote" value="1"
${helpers.checked(config['jellyfin_is_remote'])}> Remote Server
<input type="hidden" id="jellyfin_is_remote" name="jellyfin_is_remote"
value="${config['jellyfin_is_remote']}">
</label>
</div>
</div>
</div>
</div>
<input type="hidden" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" id="pms_ip" name="pms_ip" value="${config['pms_ip']}">
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a>
<span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
<input type="hidden" id="jellyfin_ip" name="jellyfin_ip" value="${config['jellyfin_ip']}">
</div>
<div class="wizard-card" data-cardname="card4">
<h3>Jellyfin Auth</h3>
<div class="wizard-input-section">
<p class="help-block">
Authentificate with your Jellyfin instance.
</p>
</div>
<div class="wizard-input-section">
<label for="jellyfin_user">Jellyfin Username</label>
<div class="row">
<div class="col-xs-4">
<input type="text" class="form-control jellyfin-settings" name="jellyfin_user"
id="jellyfin_user" value="" required>
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="jellyfin_password">Jellyfin Password</label>
<div class="row">
<div class="col-xs-4">
<input type="password" class="form-control jellyfin-settings" name="jellyfin_password"
id="jellyfin_password" value="" required>
</div>
</div>
</div>
<a class="btn btn-dark" id="verify-jellyfin-server" href="#" role="button">Verify</a>
<span style="margin-left: 10px; display: none;" id="jellyfin-verify-status"></span>
</div>
<div class="wizard-card" data-cardname="card5">
<h3>Activity Logging</h3>
<div class="wizard-input-section">
<p class="help-block">
Tautulli will keep a history of all streaming activity on your Plex server.
JellyPy will keep a history of all streaming activity on your Jellyfin server.
</p>
</div>
<div class="wizard-input-section">
<label for="logging_ignore_interval">Ignore Interval</label>
<div class="row">
<div class="col-xs-4">
<input type="text" class="form-control pms-monitoring" id="logging_ignore_interval" name="logging_ignore_interval" placeholder="120" value="${config['logging_ignore_interval']}" data-validate="validateIgnoreInterval" required>
<input type="text" class="form-control jellyfin-monitoring" id="logging_ignore_interval"
name="logging_ignore_interval" placeholder="120"
value="${config['logging_ignore_interval']}"
data-validate="validateIgnoreInterval" required>
</div>
<span style="margin-left: 10px; line-height: 35px; display: none;" id="ignore-int-status"></span>
<span style="margin-left: 10px; line-height: 35px; display: none;"
id="ignore-int-status"></span>
</div>
<p class="help-block">The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.</p>
<p class="help-block">The interval (in seconds) an item must be in a playing state before
logging it. 0 to disable.</p>
</div>
<div class="wizard-input-section">
<p class="help-block">
Additional options to disable history logging for certain libraries or users can be found by editing them
Additional options to disable history logging for certain libraries or users can be found by
editing them
on the <strong>Libraries</strong> or <strong>Users</strong> pages.
</p>
</div>
@@ -190,24 +215,13 @@
<h3>Notifications</h3>
<div class="wizard-input-section">
<p class="help-block">
Tautulli can send a wide variety of notifications to alert you of activity on your Plex server.
JellyPy can send a wide variety of notifications to alert you of activity on your Jellyfin
server.
</p>
<p class="help-block">
To set up a notification agent, navigate to the <strong>Settings</strong> page
and to the <strong>Notification Agents</strong> tab after you have completed this setup wizard.
</p>
</div>
</div>
<div class="wizard-card" data-cardname="card7">
<h3>Database Import</h3>
<div class="wizard-input-section">
<p class="help-block">
If you have an existing Tautulli, PlexWatch, or Plexivity database, you can import the data into Tautulli.
</p>
<p class="help-block">
To import a database, navigate to the <strong>Settings</strong> page
and to the <strong>Import & Backups</strong> tab after you have completed this setup wizard.
and to the <strong>Notification Agents</strong> tab after you have completed this setup
wizard.
</p>
</div>
@@ -215,40 +229,46 @@
<div style="display: none;">
<input type="checkbox" name="first_run" id="first_run" value="1" checked>
<input type="checkbox" name="group_history_tables" id="group_history_tables" value="1" checked>
<input type="checkbox" name="history_table_activity" id="history_table_activity" value="1" checked>
<input type="checkbox" name="history_table_activity" id="history_table_activity" value="1"
checked>
<input type="checkbox" name="sys_tray_icon" id="sys_tray_icon" value="1" checked>
<input type="checkbox" name="launch_startup" id="launch_startup" value="1" checked>
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" checked>
<input type="checkbox" name="api_enabled" id="api_enabled" value="1" checked>
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" checked>
<input type="checkbox" name="refresh_libraries_on_startup" id="refresh_libraries_on_startup" value="1" checked>
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1"
checked>
<input type="checkbox" name="refresh_libraries_on_startup" id="refresh_libraries_on_startup"
value="1" checked>
<input type="checkbox" name="check_github" id="check_github" value="1" checked>
<input type="checkbox" name="log_blacklist" id="log_blacklist" value="1" checked>
<input type="checkbox" name="cache_images" id="cache_images" value="1" checked>
<input type="checkbox" name="notify_group_recently_added_grandparent" id="notify_group_recently_added_grandparent" value="1" checked>
<input type="checkbox" name="notify_group_recently_added_parent" id="notify_group_recently_added_parent" value="1" checked>
<input type="checkbox" name="notify_group_recently_added_grandparent"
id="notify_group_recently_added_grandparent" value="1" checked>
<input type="checkbox" name="notify_group_recently_added_parent"
id="notify_group_recently_added_parent" value="1" checked>
<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="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
<input type="text" name="home_library_cards" id="home_library_cards" value="first_run_wizard">
</div>
</div>
</form>
<div class="wizard-success">
<h3>Setup Complete!</h3>
<br/>
<p>Setup is now complete. For more configuration options please visit the Settings menu on the home page.</p>
<p>Setup is now complete. For more configuration options please visit the Settings menu on the home
page.</p>
<br/>
<i class="fa fa-refresh fa-spin"></i>&nbspWaiting <span class="countdown">5</span> seconds to ensure authentication token is registered...
<i class="fa fa-refresh fa-spin"></i>&nbspWaiting <span class="countdown">5</span> seconds to ensure
authentication token is registered...
</div>
</div>
</div>
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
<script src="${http_root}js/bootstrap.min.js"></script>
<script src="${http_root}js/selectize.min.js"></script>
<script src="${http_root}js/platform.min.js"></script>
@@ -258,7 +278,6 @@
function validateAuthentication(el) {
var http_username = $("#http_username").val();
var http_password = $("#http_password").val();
var valid_authentication = el.val();
var retValue = {};
if (http_username === "" || http_password === "") {
@@ -273,15 +292,15 @@
return retValue;
}
function validatePMSip(el) {
var valid_pms_ip = el.val();
function validateJellyfinIp(el) {
var valid_jellyfin_ip = el.val();
var retValue = {};
if (valid_pms_ip === "") {
if (valid_jellyfin_ip === "") {
retValue.status = false;
retValue.msg = "Please verify your server.";
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please verify your server.');
$('#pms-verify-status').fadeIn('fast').delay(2000).fadeOut('fast');
$("#jellyfin-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please verify your server.');
$('#jellyfin-verify-status').fadeIn('fast').delay(2000).fadeOut('fast');
} else {
retValue.status = true;
}
@@ -289,15 +308,15 @@
return retValue;
}
function validatePMStoken(el) {
var valid_pms_token = el.val();
function validateJellyfinToken(el) {
var valid_jellyfin_token = el.val();
var retValue = {};
if (valid_pms_token === "") {
if (valid_jellyfin_token === "") {
retValue.status = false;
retValue.msg = "Please authenticate.";
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Please authenticate.');
$('#pms-token-status').fadeIn('fast').delay(2000).fadeOut('fast');
$("#jellyfin-token-status").html('<i class="fa fa-exclamation-circle"></i> Please authenticate.');
$('#jellyfin-token-status').fadeIn('fast').delay(2000).fadeOut('fast');
} else {
retValue.status = true;
}
@@ -325,71 +344,73 @@
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
}
$(document).ready(function() {
$(document).ready(function () {
$.fn.wizard.logging = false;
var options = {
keyboard : false,
contentHeight : 450,
contentWidth : 700,
backdrop: 'static',
buttons: {submitText: 'Finish'},
submitUrl: "configUpdate"
};
var wizard = $("#setup-wizard").wizard(options);
wizard.show();
$.fn.wizard.logging = false;
var options = {
keyboard: false,
contentHeight: 450,
contentWidth: 700,
backdrop: 'static',
buttons: {submitText: 'Finish'},
submitUrl: "configUpdate"
};
var wizard = $("#setup-wizard").wizard(options);
wizard.show();
// Change button classes
wizard.find('.wizard-back').addClass('btn-dark');
wizard.on('incrementCard', function(wizard) {
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
});
wizard.on('decrementCard', function(wizard) {
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
});
// Change button classes
wizard.find('.wizard-back').addClass('btn-dark');
wizard.on('incrementCard', function (wizard) {
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
});
wizard.on('decrementCard', function (wizard) {
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
});
wizard.on("submit", function(wizard) {
// Probably should not success before we know, but hopefully validation is good enough.
wizard.submitSuccess();
$.ajax({
type: "POST",
url: wizard.args.submitUrl,
data: wizard.serialize(),
dataType: "json",
complete: function (data) {
$(".countdown").countdown(function () { location.reload(); }, 5, "");
}
})
});
$('.checkbox-toggle').click(function () {
var configToggle = $(this).data('id');
if ($(this).is(':checked')) {
$('#'+configToggle).val(1);
} else {
$('#'+configToggle).val(0);
}
});
var $select_pms = $('#pms_ip_selectize').selectize({
createOnBlur: true,
openOnFocus: true,
maxItems: 1,
closeAfterSelect: true,
sortField: 'label',
searchField: ['label', 'value'],
inputClass: 'form-control selectize-input',
render: {
item: function (item, escape) {
if (!item.label) {
$.extend(item,
$(this.revertSettings.$children)
.filter('[value="' + item.value + '"]').data()
);
wizard.on("submit", function (wizard) {
// Probably should not success before we know, but hopefully validation is good enough.
wizard.submitSuccess();
$.ajax({
type: "POST",
url: wizard.args.submitUrl,
data: wizard.serialize(),
dataType: "json",
complete: function (data) {
$(".countdown").countdown(function () {
location.reload();
}, 5, "");
}
var label = item.label || item.value;
var caption = item.label ? item.ip : null;
return '<div data-identifier="' + item.clientIdentifier +
})
});
$('.checkbox-toggle').click(function () {
var configToggle = $(this).data('id');
if ($(this).is(':checked')) {
$('#' + configToggle).val(1);
} else {
$('#' + configToggle).val(0);
}
});
var $select_jellyfin = $('#jellyfin_ip_selectize').selectize({
createOnBlur: true,
openOnFocus: true,
maxItems: 1,
closeAfterSelect: true,
sortField: 'label',
searchField: ['label', 'value'],
inputClass: 'form-control selectize-input',
render: {
item: function (item, escape) {
if (!item.label) {
$.extend(item,
$(this.revertSettings.$children)
.filter('[value="' + item.value + '"]').data()
);
}
var label = item.label || item.value;
var caption = item.label ? item.ip : null;
return '<div data-identifier="' + item.clientIdentifier +
'" data-ip="' + item.ip +
'" data-port="' + item.port +
'" data-local="' + item.local +
@@ -399,11 +420,11 @@ $(document).ready(function() {
'<span class="item-text">' + escape(label) + '</span>' +
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
'</div>';
},
option: function (item, escape) {
var label = item.label || item.value;
var caption = item.label ? item.value : null;
return '<div data-identifier="' + item.clientIdentifier +
},
option: function (item, escape) {
var label = item.label || item.value;
var caption = item.label ? item.value : null;
return '<div data-identifier="' + item.clientIdentifier +
'" data-ip="' + item.ip +
'" data-port="' + item.port +
'" data-local="' + item.local +
@@ -413,152 +434,131 @@ $(document).ready(function() {
escape(label) +
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
'</div>';
}
},
create: function(input) {
return {label: '', value: input};
},
onInitialize: function () {
var s = this;
this.revertSettings.$children.each(function () {
$.extend(s.options[this.value], $(this).data());
});
},
onChange: function (item) {
var pms_ip_selected = this.getItem(item)[0];
var identifier = $(pms_ip_selected).data('identifier');
var ip = $(pms_ip_selected).data('ip');
var port = $(pms_ip_selected).data('port');
var local = $(pms_ip_selected).data('local');
var ssl = $(pms_ip_selected).data('ssl');
var is_cloud = $(pms_ip_selected).data('is_cloud');
var value = $(pms_ip_selected).data('value');
$("#pms_valid").val(identifier !== 'undefined' ? 'valid' : '');
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i>&nbsp; Server found!' : '').fadeIn('fast');
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
$('#pms_ip').val(ip !== 'undefined' ? ip : value);
$('#pms_port').val(port !== 'undefined' ? port : 32400);
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
if (is_cloud === true) {
$('#pms_port').prop('readonly', true);
$('#pms_is_remote_checkbox').prop('disabled', true);
$('#pms_ssl_checkbox').prop('disabled', true);
} else {
$('#pms_port').prop('readonly', false);
$('#pms_is_remote_checkbox').prop('disabled', false);
$('#pms_ssl_checkbox').prop('disabled', false);
}
},
onDropdownOpen: function() {
this.clear();
}
});
var select_pms = $select_pms[0].selectize;
function getServerOptions(token) {
/* Set token and returns server options */
$.ajax({
url: 'discover',
data: {
token: token
}
},
success: function (result) {
if (result) {
var existing_ip = $('#pms_ip').val();
var existing_port = $('#pms_port').val();
result.forEach(function (item) {
if (item.ip === existing_ip && item.port === existing_port) {
select_pms.updateOption(item.value, item);
} else {
select_pms.addOption(item);
create: function (input) {
return {label: '', value: input};
},
onInitialize: function () {
var s = this;
this.revertSettings.$children.each(function () {
$.extend(s.options[this.value], $(this).data());
});
},
onChange: function (item) {
var jellyfin_ip_selected = this.getItem(item)[0];
var identifier = $(jellyfin_ip_selected).data('identifier');
var ip = $(jellyfin_ip_selected).data('ip');
var port = $(jellyfin_ip_selected).data('port');
var local = $(jellyfin_ip_selected).data('local');
var ssl = $(jellyfin_ip_selected).data('ssl');
var value = $(jellyfin_ip_selected).data('value');
$("#jellyfin_valid").val(identifier !== 'undefined' ? 'valid' : '');
$("#jellyfin-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i>&nbsp; Server found!' : '').fadeIn('fast');
$("#jellyfin_identifier").val(identifier !== 'undefined' ? identifier : '');
$('#jellyfin_ip').val(ip !== 'undefined' ? ip : value);
$('#jellyfin_port').val(port !== 'undefined' ? port : 8096);
$('#jellyfin_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
$('#jellyfin_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
$('#jellyfin_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
$('#jellyfin_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
$('#jellyfin_port').prop('readonly', false);
$('#jellyfin_is_remote_checkbox').prop('disabled', false);
$('#jellyfin_ssl_checkbox').prop('disabled', false);
},
onDropdownOpen: function () {
this.clear();
}
});
var select_jellyfin = $select_jellyfin[0].selectize;
function getServerOptions(token) {
/* Set token and returns server options */
$.ajax({
url: 'discover',
data: {
token: token
},
success: function (result) {
if (result) {
var existing_ip = $('#jellyfin_ip').val();
var existing_port = $('#jellyfin_port').val();
result.forEach(function (item) {
if (item.ip === existing_ip && item.port === existing_port) {
select_jellyfin.updateOption(item.value, item);
} else {
select_jellyfin.addOption(item);
}
});
}
}
})
}
var jellyfin_verified = false;
var authenticated = false;
$("#verify-jellyfin-server").click(function () {
if (!(jellyfin_verified)) {
var jellyfin_ip = $("#jellyfin_ip").val().trim();
var jellyfin_port = $("#jellyfin_port").val().trim();
var jellyfin_ssl = $("#jellyfin_ssl").val();
var jellyfin_is_remote = $("#jellyfin_is_remote").val();
var jellyfin_user = $("#jellyfin_user").val().trim();
var jellyfin_password = $("#jellyfin_password").val();
if ((jellyfin_ip !== '') || (jellyfin_port !== '')) {
$("#jellyfin-verify-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Verifying server...');
$('#jellyfin-verify-status').fadeIn('fast');
$.ajax({
url: 'check_login',
data: {
hostname: jellyfin_ip,
port: jellyfin_port,
ssl: jellyfin_ssl,
remote: jellyfin_is_remote,
user: jellyfin_user,
password: jellyfin_password
},
cache: true,
async: true,
timeout: 5000,
error: function (jqXHR, textStatus, errorThrown) {
$("#jellyfin-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Error verifying server: ' + textStatus);
$('#jellyfin-verify-status').fadeIn('fast');
},
success: function (xhr, status) {
var result = xhr;
var identifier = result.identifier;
if (identifier) {
$("#jellyfin_identifier").val(identifier);
$("#jellyfin-verify-status").html('<i class="fa fa-check"></i>&nbsp; Login successfull!');
$('#jellyfin-verify-status').fadeIn('fast');
jellyfin_verified = true;
$("#jellyfin_valid").val("valid");
} else {
$("#jellyfin-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; This is not a Jellyfin Server!');
$('#jellyfin-verify-status').fadeIn('fast');
}
}
});
} else {
$("#jellyfin-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Please enter both fields.');
$('#jellyfin-verify-status').fadeIn('fast');
}
}
})
}
var pms_verified = false;
var authenticated = false;
});
$("#verify-plex-server").click(function () {
if (!(pms_verified)) {
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();
if ((pms_ip !== '') || (pms_port !== '')) {
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Verifying server...');
$('#pms-verify-status').fadeIn('fast');
$.ajax({
url: 'get_server_id',
data: {
hostname: pms_ip,
port: pms_port,
identifier: pms_identifier,
ssl: pms_ssl,
remote: pms_is_remote
},
cache: true,
async: true,
timeout: 5000,
error: function (jqXHR, textStatus, errorThrown) {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Error verifying server: ' + textStatus);
$('#pms-verify-status').fadeIn('fast');
},
success: function(xhr, status) {
var result = xhr;
var identifier = result.identifier;
if (identifier) {
$("#pms_identifier").val(identifier);
$("#pms-verify-status").html('<i class="fa fa-check"></i>&nbsp; Server found!');
$('#pms-verify-status').fadeIn('fast');
pms_verified = true;
$("#pms_valid").val("valid");
} else {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast');
}
}
});
} else {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Please enter both fields.');
$('#pms-verify-status').fadeIn('fast');
}
}
$(".jellyfin-settings").change(function () {
jellyfin_verified = false;
$("#jellyfin_valid").val("");
$("#jellyfin-verify-status").html("");
});
});
$( ".pms-settings" ).change(function() {
pms_verified = false;
$("#pms_valid").val("");
$("#pms-verify-status").html("");
});
function OAuthPreFunction() {
$("#pms_token").val('');
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Waiting for authentication...').fadeIn('fast');
}
function OAuthSuccessCallback(authToken) {
$("#pms_token").val(authToken);
$("#pms-token-status").html('<i class="fa fa-check"></i>&nbsp; Authentication successful.').fadeIn('fast');
authenticated = true;
getServerOptions(authToken);
}
function OAuthErrorCallback() {
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Error communicating with Plex.tv.').fadeIn('fast');
}
$('#sign-in-plex').click(function() {
PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback, OAuthPreFunction);
});
});
</script>
</body>

View File

@@ -1,11 +1,11 @@
% if data:
<%
import plexpy
from plexpy.helpers import grouper, get_img_service
import jellypy
from jellypy.helpers import grouper, get_img_service
recently_added = data['recently_added']
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
if jellypy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
base_url = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'newsletter/'
elif preview:
base_url = 'newsletter/'
else:
@@ -13,7 +13,7 @@
service = get_img_service(include_self=True)
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/'
base_url_image = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'image/'
elif preview and service and service != 'self-hosted':
base_url_image = 'image/'
else:
@@ -521,7 +521,7 @@
line-height: 100%;
}
.apple-link a {
a[x-apple-data-detectors] {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
@@ -546,7 +546,7 @@
<span class="preheader" style="color: transparent;display: none;height: 0;max-height: 0;max-width: 0;opacity: 0;overflow: hidden;mso-hide: all;visibility: hidden;width: 0;">Tautulli Newsletter - ${subject}</span>
% if base_url and not preview:
<div class="view-full" style="clear: both;color: #282A2D;font-size: 12px;margin-bottom: 10px;text-align: center;width: 100%;"> <!-- IGNORE SAVE -->
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" rel="noreferrer" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
</div> <!-- IGNORE SAVE -->
% endif
<table border="0" cellpadding="3" cellspacing="0" class="main" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background: #282A2D;border-radius: 3px;color: #ffffff;">
@@ -599,7 +599,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" style="text-decoration: underline;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
</a>
</td>
@@ -610,7 +610,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
<tr>
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${movie['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${movie['title']}</a>
</td>
</tr>
<tr>
@@ -728,7 +728,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" style="text-decoration: underline;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
</a>
</td>
@@ -739,7 +739,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
<tr>
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${show['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${show['title']}</a>
</td>
</tr>
<tr>
@@ -872,7 +872,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" style="text-decoration: underline;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-cover.png'}" width="150" height="150" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
</a>
</td>
@@ -883,7 +883,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
<tr>
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${album['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${album['title']}</a>
</td>
</tr>
<tr>
@@ -986,7 +986,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + video['thumb_hash']) if base_url_image else video['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" style="text-decoration: underline;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
</a>
</td>
@@ -997,7 +997,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
<tr>
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${video['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${video['title']}</a>
</td>
</tr>
<tr>
@@ -1087,4 +1087,4 @@
</table>
</body>
</html>
% endif
% endif

View File

@@ -1,19 +1,19 @@
% if data:
<%
import plexpy
from plexpy.helpers import grouper, get_img_service
import jellypy
from jellypy.helpers import grouper, get_img_service
recently_added = data['recently_added']
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
if jellypy.CONFIG.NEWSLETTER_SELF_HOSTED and jellypy.CONFIG.HTTP_BASE_URL:
base_url = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'newsletter/'
elif preview:
base_url = 'newsletter/'
else:
base_url = ''
service = get_img_service(include_self=True)
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/'
if service == 'self-hosted' and jellypy.CONFIG.HTTP_BASE_URL:
base_url_image = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'image/'
elif preview and service and service != 'self-hosted':
base_url_image = 'image/'
else:
@@ -521,7 +521,7 @@
line-height: 100%;
}
.apple-link a {
a[x-apple-data-detectors] {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
@@ -546,7 +546,7 @@
<span class="preheader">Tautulli Newsletter - ${subject}</span>
% if base_url and not preview:
<div class="view-full"> <!-- IGNORE SAVE -->
<a href="${base_url + uuid}" title="View full newsletter" target="_blank">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" rel="noreferrer">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
</div> <!-- IGNORE SAVE -->
% endif
<table border="0" cellpadding="3" cellspacing="0" class="main">
@@ -599,7 +599,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']})">
<tr>
<td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
</a>
</td>
@@ -610,7 +610,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
<tr>
<td class="card-info-title nowrap">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">${movie['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer">${movie['title']}</a>
</td>
</tr>
<tr>
@@ -728,7 +728,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']})">
<tr>
<td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
</a>
</td>
@@ -739,7 +739,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
<tr>
<td class="card-info-title nowrap">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">${show['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer">${show['title']}</a>
</td>
</tr>
<tr>
@@ -872,7 +872,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']})">
<tr>
<td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-cover.png'}" width="150" height="150">
</a>
</td>
@@ -883,7 +883,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
<tr>
<td class="card-info-title nowrap">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">${album['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer">${album['title']}</a>
</td>
</tr>
<tr>
@@ -986,7 +986,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + video['thumb_hash']) if base_url_image else video['thumb_url']})">
<tr>
<td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer">
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
</a>
</td>
@@ -997,7 +997,7 @@
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
<tr>
<td class="card-info-title nowrap">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank">${video['title']}</a>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer">${video['title']}</a>
</td>
</tr>
<tr>
@@ -1087,4 +1087,4 @@
</table>
</body>
</html>
% endif
% endif

View File

@@ -38,6 +38,7 @@ load_rc_config ${name}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command_interpreter="python"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"

View File

@@ -28,14 +28,16 @@
# Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli
# 2. Give the user ownership of the Tautulli directory:
# sudo chown tautulli:tautulli -R /opt/Tautulli
# sudo chown -R tautulli:tautulli /opt/Tautulli
#
# - Adjust ExecStart= to point to:
# 1. Your Tautulli executable
# 1. Your Python interpreter (get the path with "command -v python3")
# - Default: /usr/bin/python3
# 2. Your Tautulli executable
# - Default: /opt/Tautulli/Tautulli.py
# 2. Your config file (recommended is to put it somewhere in /etc)
# 3. Your config file (recommended is to put it somewhere in /etc)
# - Default: --config /opt/Tautulli/config.ini
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# 4. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# - Default: --datadir /opt/Tautulli
#
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
@@ -50,9 +52,7 @@ Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
GuessMainPID=no
Type=forking
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --nolaunch
User=tautulli
Group=tautulli
Restart=on-abnormal

Some files were not shown because too many files have changed in this diff Show More