Compare commits
100 Commits
v2.1.2-bet
...
v2.1.10-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b5f2f55972 | ||
![]() |
ac207260c8 | ||
![]() |
e93808381c | ||
![]() |
7acb8f7dc5 | ||
![]() |
ba9f4a1f9e | ||
![]() |
8502c28e25 | ||
![]() |
10add90451 | ||
![]() |
ddb7fa04ca | ||
![]() |
e21a13b7ff | ||
![]() |
1245b4fbd3 | ||
![]() |
94b00c75c2 | ||
![]() |
2edcf26110 | ||
![]() |
a9fdf73e8b | ||
![]() |
4884cee309 | ||
![]() |
b3c7256bcf | ||
![]() |
2c9a7ced13 | ||
![]() |
aa365eb6a3 | ||
![]() |
2366a8811b | ||
![]() |
53aafbd19e | ||
![]() |
d5bffc374c | ||
![]() |
5cd5c36d8c | ||
![]() |
7f9e8f6211 | ||
![]() |
f743a817ba | ||
![]() |
8e4aba7ed4 | ||
![]() |
8c0ef75d4c | ||
![]() |
76c4b3bb71 | ||
![]() |
112b1c7984 | ||
![]() |
c22a2513e3 | ||
![]() |
f336782fc1 | ||
![]() |
c19afa06de | ||
![]() |
e003850d31 | ||
![]() |
23cf790079 | ||
![]() |
e7f930bd0f | ||
![]() |
348707b6b9 | ||
![]() |
7ad78b4536 | ||
![]() |
a408a62234 | ||
![]() |
a1e9e7e87f | ||
![]() |
fa99f6e684 | ||
![]() |
11e9bd2d54 | ||
![]() |
50165af4b7 | ||
![]() |
5dd22c23f2 | ||
![]() |
79b45c1c46 | ||
![]() |
af917c4915 | ||
![]() |
c3238b5a83 | ||
![]() |
908dbc3243 | ||
![]() |
14b6df8c25 | ||
![]() |
d3e53cb97f | ||
![]() |
445eea5c1e | ||
![]() |
c5918d7d6c | ||
![]() |
b8e025193e | ||
![]() |
85772cdd83 | ||
![]() |
7f2bab3082 | ||
![]() |
8185cc1c40 | ||
![]() |
63bfe96124 | ||
![]() |
88b640f5e2 | ||
![]() |
26b2342956 | ||
![]() |
883280be09 | ||
![]() |
7c76b0678a | ||
![]() |
e91ba46265 | ||
![]() |
62104c95e3 | ||
![]() |
178bd89e7c | ||
![]() |
365260401c | ||
![]() |
04029bd4d3 | ||
![]() |
9cf1128712 | ||
![]() |
2eebce9f6c | ||
![]() |
b08e071b81 | ||
![]() |
7778d84728 | ||
![]() |
8e3fe7bfa2 | ||
![]() |
6f22c823be | ||
![]() |
34d7c67813 | ||
![]() |
862ed5ce9f | ||
![]() |
84406e6797 | ||
![]() |
19cf567366 | ||
![]() |
8af697a157 | ||
![]() |
76122bea5d | ||
![]() |
1a12422908 | ||
![]() |
2df9f0b48b | ||
![]() |
8540b80e57 | ||
![]() |
8ad565a444 | ||
![]() |
f91b6481b3 | ||
![]() |
826db082c9 | ||
![]() |
f3d64a7886 | ||
![]() |
031d078bc2 | ||
![]() |
04fcd78102 | ||
![]() |
53d1e0f541 | ||
![]() |
9719f0b25b | ||
![]() |
6d1d5bc822 | ||
![]() |
0d7bbe044d | ||
![]() |
b1dc5816a4 | ||
![]() |
476011a783 | ||
![]() |
e038c57c4c | ||
![]() |
a989a53750 | ||
![]() |
d8cfdea704 | ||
![]() |
ed4722c4ce | ||
![]() |
17ab5f05ed | ||
![]() |
71ab2248d7 | ||
![]() |
4fb4410552 | ||
![]() |
a915d2333f | ||
![]() |
aaf5a18251 | ||
![]() |
b90026801b |
300
API.md
300
API.md
@@ -32,6 +32,21 @@ General optional parameters:
|
||||
|
||||
## API methods
|
||||
|
||||
### add_newsletter_config
|
||||
Add a new notification agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
agent_id (int): The newsletter type to add
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### add_notifier_config
|
||||
Add a new notification agent.
|
||||
|
||||
@@ -93,27 +108,30 @@ Returns:
|
||||
Delete and recreate the cache directory.
|
||||
|
||||
|
||||
### delete_image_cache
|
||||
Delete and recreate the image cache directory.
|
||||
|
||||
|
||||
### delete_imgur_poster
|
||||
Delete the Imgur poster.
|
||||
### delete_hosted_images
|
||||
Delete the images uploaded to image hosting services.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
rating_key (int): 1234
|
||||
(Note: Must be the movie, show, season, artist, or album rating key)
|
||||
Optional parameters:
|
||||
None
|
||||
service (str): 'imgur' or 'cloudinary'
|
||||
delete_all (bool): 'true' to delete all images form the service
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"result": "success",
|
||||
"message": "Deleted Imgur poster."}
|
||||
"message": "Deleted hosted images from Imgur."}
|
||||
```
|
||||
|
||||
|
||||
### delete_image_cache
|
||||
Delete and recreate the image cache directory.
|
||||
|
||||
|
||||
### delete_library
|
||||
Delete a library section from Tautulli. Also erases all history for the library.
|
||||
|
||||
@@ -191,6 +209,36 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### delete_newsletter
|
||||
Remove a newsletter from the database.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The newsletter to delete
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### delete_newsletter_log
|
||||
Delete the Tautulli newsletter logs.
|
||||
|
||||
```
|
||||
Required paramters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### delete_notification_log
|
||||
Delete the Tautulli notification logs.
|
||||
|
||||
@@ -386,6 +434,7 @@ Returns:
|
||||
"optimized_version_profile": "",
|
||||
"optimized_version_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"original_title": "",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
||||
@@ -630,6 +679,7 @@ Returns:
|
||||
"full_title": "Game of Thrones - The Red Woman",
|
||||
"grandparent_rating_key": 351,
|
||||
"grandparent_title": "Game of Thrones",
|
||||
"original_title": "",
|
||||
"group_count": 1,
|
||||
"group_ids": "1124",
|
||||
"id": 1124,
|
||||
@@ -916,9 +966,9 @@ Optional parameters:
|
||||
|
||||
Returns:
|
||||
json:
|
||||
[{"section_id": 1, "section_name": "Movies"},
|
||||
{"section_id": 7, "section_name": "Music"},
|
||||
{"section_id": 2, "section_name": "TV Shows"},
|
||||
[{"section_id": 1, "section_name": "Movies", "section_type": "movie"},
|
||||
{"section_id": 7, "section_name": "Music", "section_type": "artist"},
|
||||
{"section_id": 2, "section_name": "TV Shows", "section_type": "show"},
|
||||
{...}
|
||||
]
|
||||
```
|
||||
@@ -1124,6 +1174,7 @@ Returns:
|
||||
}
|
||||
],
|
||||
"media_type": "episode",
|
||||
"original_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
@@ -1166,6 +1217,109 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### get_newsletter_config
|
||||
Get the configuration for an existing notification agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The newsletter config to retrieve
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"id": 1,
|
||||
"agent_id": 0,
|
||||
"agent_name": "recently_added",
|
||||
"agent_label": "Recently Added",
|
||||
"friendly_name": "",
|
||||
"id_name": "",
|
||||
"cron": "0 0 * * 1",
|
||||
"active": 1,
|
||||
"subject": "Recently Added to {server_name}! ({end_date})",
|
||||
"body": "View the newsletter here: {newsletter_url}",
|
||||
"message": "",
|
||||
"config": {"custom_cron": 0,
|
||||
"filename": "newsletter_{newsletter_uuid}.html",
|
||||
"formatted": 1,
|
||||
"incl_libraries": ["1", "2"],
|
||||
"notifier_id": 1,
|
||||
"save_only": 0,
|
||||
"time_frame": 7,
|
||||
"time_frame_units": "days"
|
||||
},
|
||||
"email_config": {...},
|
||||
"config_options": [{...}, ...],
|
||||
"email_config_options": [{...}, ...]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### get_newsletter_log
|
||||
Get the data on the Tautulli newsletter logs table.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
order_column (str): "timestamp", "newsletter_id", "agent_name", "notify_action",
|
||||
"subject_text", "start_date", "end_date", "uuid"
|
||||
order_dir (str): "desc" or "asc"
|
||||
start (int): Row to start from, 0
|
||||
length (int): Number of items to return, 25
|
||||
search (str): A string to search for, "Telegram"
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"draw": 1,
|
||||
"recordsTotal": 1039,
|
||||
"recordsFiltered": 163,
|
||||
"data":
|
||||
[{"agent_id": 0,
|
||||
"agent_name": "recently_added",
|
||||
"end_date": "2018-03-18",
|
||||
"id": 7,
|
||||
"newsletter_id": 1,
|
||||
"notify_action": "on_cron",
|
||||
"start_date": "2018-03-05",
|
||||
"subject_text": "Recently Added to Plex (Winterfell-Server)! (2018-03-18)",
|
||||
"success": 1,
|
||||
"timestamp": 1462253821,
|
||||
"uuid": "7fe4g65i"
|
||||
},
|
||||
{...},
|
||||
{...}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### get_newsletters
|
||||
Get a list of configured newsletters.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json:
|
||||
[{"id": 1,
|
||||
"agent_id": 0,
|
||||
"agent_name": "recently_added",
|
||||
"agent_label": "Recently Added",
|
||||
"friendly_name": "",
|
||||
"cron": "0 0 * * 1",
|
||||
"active": 1
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
### get_notification_log
|
||||
Get the data on the Tautulli notification logs table.
|
||||
|
||||
@@ -1174,8 +1328,8 @@ Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
order_column (str): "timestamp", "agent_name", "notify_action",
|
||||
"subject_text", "body_text", "script_args"
|
||||
order_column (str): "timestamp", "notifier_id", "agent_name", "notify_action",
|
||||
"subject_text", "body_text",
|
||||
order_dir (str): "desc" or "asc"
|
||||
start (int): Row to start from, 0
|
||||
length (int): Number of items to return, 25
|
||||
@@ -1188,15 +1342,14 @@ Returns:
|
||||
"recordsFiltered": 163,
|
||||
"data":
|
||||
[{"agent_id": 13,
|
||||
"agent_name": "Telegram",
|
||||
"body_text": "Game of Thrones - S06E01 - The Red Woman [Transcode].",
|
||||
"agent_name": "telegram",
|
||||
"body_text": "DanyKhaleesi69 started playing The Red Woman.",
|
||||
"id": 1000,
|
||||
"notify_action": "play",
|
||||
"poster_url": "http://i.imgur.com/ZSqS8Ri.jpg",
|
||||
"notify_action": "on_play",
|
||||
"rating_key": 153037,
|
||||
"script_args": "[]",
|
||||
"session_key": 147,
|
||||
"subject_text": "Tautulli (Winterfell-Server)",
|
||||
"success": 1,
|
||||
"timestamp": 1462253821,
|
||||
"user": "DanyKhaleesi69",
|
||||
"user_id": 8008135
|
||||
@@ -1629,6 +1782,7 @@ Returns:
|
||||
"library_name": "",
|
||||
"media_index": "1",
|
||||
"media_type": "episode",
|
||||
"original_title": "",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
||||
@@ -1777,6 +1931,69 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### get_stream_data
|
||||
Get the stream details from history or current stream.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
row_id (int): The row ID number for the history item, OR
|
||||
session_key (int): The session key of the current stream
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"aspect_ratio": "2.35",
|
||||
"audio_bitrate": 231,
|
||||
"audio_channels": 6,
|
||||
"audio_codec": "aac",
|
||||
"audio_decision": "transcode",
|
||||
"bitrate": 2731,
|
||||
"container": "mp4",
|
||||
"current_session": "",
|
||||
"grandparent_title": "",
|
||||
"media_type": "movie",
|
||||
"optimized_version": "",
|
||||
"optimized_version_profile": "",
|
||||
"optimized_version_title": "",
|
||||
"original_title": "",
|
||||
"pre_tautulli": "",
|
||||
"quality_profile": "1.5 Mbps 480p",
|
||||
"stream_audio_bitrate": 203,
|
||||
"stream_audio_channels": 2,
|
||||
"stream_audio_codec": "aac",
|
||||
"stream_audio_decision": "transcode",
|
||||
"stream_bitrate": 730,
|
||||
"stream_container": "mkv",
|
||||
"stream_container_decision": "transcode",
|
||||
"stream_subtitle_codec": "",
|
||||
"stream_subtitle_decision": "",
|
||||
"stream_video_bitrate": 527,
|
||||
"stream_video_codec": "h264",
|
||||
"stream_video_decision": "transcode",
|
||||
"stream_video_framerate": "24p",
|
||||
"stream_video_height": 306,
|
||||
"stream_video_resolution": "SD",
|
||||
"stream_video_width": 720,
|
||||
"subtitle_codec": "",
|
||||
"subtitles": "",
|
||||
"synced_version": "",
|
||||
"synced_version_profile": "",
|
||||
"title": "Frozen",
|
||||
"transcode_hw_decoding": "",
|
||||
"transcode_hw_encoding": "",
|
||||
"video_bitrate": 2500,
|
||||
"video_codec": "h264",
|
||||
"video_decision": "transcode",
|
||||
"video_framerate": "24p",
|
||||
"video_height": 816,
|
||||
"video_resolution": "1080",
|
||||
"video_width": 1920
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### get_stream_type_by_top_10_platforms
|
||||
Get graph data by stream type by top 10 platforms.
|
||||
|
||||
@@ -2215,6 +2432,23 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### notify_newsletter
|
||||
Send a newsletter using Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The ID number of the newsletter agent
|
||||
|
||||
Optional parameters:
|
||||
subject (str): The subject of the newsletter
|
||||
body (str): The body of the newsletter
|
||||
message (str): The message of the newsletter
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### notify_recently_added
|
||||
Send a recently added notification using Tautulli.
|
||||
|
||||
@@ -2244,8 +2478,12 @@ Required parameters:
|
||||
rating_key (str): 54321
|
||||
|
||||
Optional parameters:
|
||||
width (str): 150
|
||||
height (str): 255
|
||||
width (str): 300
|
||||
height (str): 450
|
||||
opacity (str): 25
|
||||
background (str): 282828
|
||||
blur (str): 3
|
||||
img_format (str): png
|
||||
fallback (str): "poster", "cover", "art"
|
||||
refresh (bool): True or False whether to refresh the image cache
|
||||
|
||||
@@ -2312,7 +2550,7 @@ Returns:
|
||||
|
||||
|
||||
### set_mobile_device_config
|
||||
Configure an exisitng notificaiton agent.
|
||||
Configure an existing notification agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
@@ -2326,8 +2564,24 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### set_newsletter_config
|
||||
Configure an existing newsletter agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The newsletter config to update
|
||||
agent_id (int): The newsletter type of the newsletter
|
||||
|
||||
Optional parameters:
|
||||
Pass all the config options for the agent with the 'newsletter_config_' and 'newsletter_email_' prefix.
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### set_notifier_config
|
||||
Configure an exisitng notificaiton agent.
|
||||
Configure an existing notification agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
|
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,5 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
## v2.1.10-beta (2018-05-28)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Improved monitoring of live tv sessions.
|
||||
* Change: Use track artist instead of album artist.
|
||||
* Notifications:
|
||||
* New: Added timestamp to Discord notification embeds. (Thanks @samwiseg00)
|
||||
* New: Enable notifications for "clip" media types.
|
||||
* Fix: Actually add the "live" notification parameter.
|
||||
* Change: Update Twitter for 280 characters.
|
||||
* Change: Use HTTPS url for Cloudinary images.
|
||||
* Newsletters:
|
||||
* Fix: Artist summaries not showing up on newsletter cards.
|
||||
* Change: Do not send the newsletter if the template fails to render.
|
||||
|
||||
|
||||
## v2.1.9 (2018-05-21)
|
||||
|
||||
* Notifications:
|
||||
* New: Added "live" to notification parameters.
|
||||
|
||||
|
||||
## v2.1.8-beta (2018-05-19)
|
||||
|
||||
* Newsletters:
|
||||
* New: Added authentication options for self-hosted newsletters.
|
||||
* Change: Check if the Tautulli footer has been removed in custom newsletter templates.
|
||||
* Notifications:
|
||||
* Fix: Cloudinary images not working for Twitter notifications.
|
||||
* API:
|
||||
* Fix: Return proper HTTP status codes for errors.
|
||||
|
||||
|
||||
## v2.1.7-beta (2018-05-13)
|
||||
|
||||
* Newsletters:
|
||||
* New: Option to toggle between inline or internal CSS style templates.
|
||||
* New: Button to delete all uploaded images from Imgur/Cloudinary.
|
||||
* Fix: Long titles overflowing the newsletter cards.
|
||||
* Change: Self-hosted images on newsletters to use the /image endpoint instead of proxying through /newsletter/image.
|
||||
* Change: Strip whitespace from newsletter for smaller file size before sending to email.
|
||||
* API:
|
||||
* New: Added get_stream_data command to API.
|
||||
* New: Added newsletter API commands to documentation.
|
||||
|
||||
|
||||
## v2.1.6-beta (2018-05-09)
|
||||
|
||||
* Newsletters:
|
||||
* Change: Setting to specify static URL ID name instead of using the newsletter ID number.
|
||||
* Change: Reorganize newsletter config options.
|
||||
|
||||
|
||||
## v2.1.5-beta (2018-05-07)
|
||||
|
||||
* Newsletters:
|
||||
* New: Added setting for a custom newsletter template folder.
|
||||
* New: Added option to enable static newsletter URLs to retrieve the last sent scheduled newsletter.
|
||||
* New: Added ability to change the newsletter output directory and filenames.
|
||||
* New: Added option to save the newsletter file without sending it to a notification agent.
|
||||
* Fix: Check for disabled image hosting setting.
|
||||
* Fix: Cache newsletter images when refreshing the page.
|
||||
* Fix: Refresh image from the Plex server when uploading to image hosting.
|
||||
* Change: Allow all image hosting options with self-hosted newsletters.
|
||||
* UI:
|
||||
* Change: Don't retrieve recently added on the homepage if the Plex Cloud server is sleeping.
|
||||
* Other:
|
||||
* Fix: Imgur database upgrade migration.
|
||||
|
||||
|
||||
## v2.1.4 (2018-05-05)
|
||||
|
||||
* Newsletters:
|
||||
* Fix: Newsletter URL without an HTTP root.
|
||||
|
||||
|
||||
## v2.1.3-beta (2018-05-04)
|
||||
|
||||
* Newsletters:
|
||||
* Fix: HTTP root doubled in newsletter URL.
|
||||
* Fix: Configuration would not open with failed hostname resolution.
|
||||
* Fix: Schedule one day off when using weekday names in cron.
|
||||
* Fix: Images not refreshing when changed in Plex.
|
||||
* Fix: Cloudinary upload with non-ASCII image titles.
|
||||
* Other:
|
||||
* Fix: Potential XSS vulnerability in search.
|
||||
|
||||
|
||||
## v2.1.2-beta (2018-05-01)
|
||||
|
||||
* Newsletters:
|
||||
|
@@ -4,12 +4,12 @@
|
||||
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 `dev` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/dev -b FEATURE_NAME`. Use meaningful commit messages.
|
||||
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. Note that Tautulli runs on different platforms, including Network Attached Storage devices such as Synology.
|
||||
The code should work with Python 2.7. 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.
|
||||
|
||||
@@ -29,13 +29,10 @@ Although Tautulli did not adapt a code convention in the past, we try to follow
|
||||
#### Documentation
|
||||
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
|
||||
|
||||
#### Continuous Integration
|
||||
Tautulli has a configuration file for [travis-ci](https://travis-ci.org/). You can add your forked repo to Travis to have it check your code against PEP8, PyLint, and PyFlakes for you. Your pull request will show a green check mark or a red cross on each tested commit, depending on if linting passes.
|
||||
|
||||
### HTML/Template code
|
||||
|
||||
#### Compatibility
|
||||
HTML5 compatible browsers are targetted. There is no specific mobile version of Tautulli yet.
|
||||
HTML5 compatible browsers are targeted.
|
||||
|
||||
#### Conventions
|
||||
* 4 space indentation
|
||||
|
@@ -27,9 +27,9 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
||||
|
||||
## Preview
|
||||
|
||||
* [Full preview gallery available on our website](http://tautulli.com)
|
||||
* [Full preview gallery available on our website](https://tautulli.com)
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation and Support
|
||||
|
||||
|
@@ -69,7 +69,7 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
<tr>
|
||||
<td>Platform:</td>
|
||||
<td>${common.PLATFORM} ${common.PLATFORM_VERSION}</td>
|
||||
<td>${common.PLATFORM} ${common.PLATFORM_RELEASE} (${common.PLATFORM_VERSION + (' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else '')})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python Version:</td>
|
||||
@@ -78,7 +78,7 @@ DOCUMENTATION :: END
|
||||
<tr>
|
||||
<td class="top-line">Resources:</td>
|
||||
<td class="top-line">
|
||||
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> |
|
||||
<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> |
|
||||
|
@@ -70,6 +70,7 @@ div.form-control .selectize-input {
|
||||
background-color: #555;
|
||||
border-radius: 3px;
|
||||
transition: background-color .3s;
|
||||
height: 32px !important;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control,
|
||||
.selectize-control.form-control .selectize-input {
|
||||
@@ -2934,6 +2935,7 @@ a .home-platforms-list-cover-face:hover
|
||||
}
|
||||
.stacked-configs > li > span > a.toggle-left,
|
||||
.stacked-configs > li > span > span.toggle-left {
|
||||
float: left;
|
||||
color: #444;
|
||||
padding-right: 8px;
|
||||
}
|
||||
@@ -2944,16 +2946,6 @@ a .home-platforms-list-cover-face:hover
|
||||
.stacked-configs > li > span > span.active {
|
||||
color: #f9be03;
|
||||
}
|
||||
.stacked-configs > li.new-notification-agent,
|
||||
.stacked-configs > li.notification-agent,
|
||||
.stacked-configs > li.add-notification-agent,
|
||||
.stacked-configs > li.new-newsletter-agent,
|
||||
.stacked-configs > li.newsletter-agent,
|
||||
.stacked-configs > li.add-newsletter-agent,
|
||||
.stacked-configs > li.mobile-device,
|
||||
.stacked-configs > li.add-mobile-device {
|
||||
cursor: pointer;
|
||||
}
|
||||
.stacked-configs > li.mobile-device > span > a.toggle-left,
|
||||
.stacked-configs > li.mobile-device > span > span.toggle-left {
|
||||
color: #999;
|
||||
@@ -3524,8 +3516,7 @@ a.no-highlight:hover {
|
||||
}
|
||||
.login-logo {
|
||||
margin: 0 auto 50px auto;
|
||||
width: 340px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
.login-container .form-group {
|
||||
margin-bottom: 20px;
|
||||
@@ -4092,4 +4083,16 @@ a:hover .overlay-refresh-image:hover {
|
||||
}
|
||||
a[data-tab-destination] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.modal-config-section {
|
||||
margin-top: 10px !important;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #444;
|
||||
}
|
||||
.newsletter-logo {
|
||||
margin: 0 auto 50px auto;
|
||||
text-align: center;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@@ -387,8 +387,8 @@ DOCUMENTATION :: END
|
||||
<a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'track':
|
||||
<a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
<a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
- <a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['original_title'] or data['grandparent_title']}">${data['original_title'] or data['grandparent_title']}</a>
|
||||
% elif data['media_type'] == 'photo':
|
||||
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
||||
% elif data['media_type'] == 'clip':
|
||||
|
@@ -5,6 +5,7 @@
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
<% from plexpy import PLEX_SERVER_UP %>
|
||||
<div class="container-fluid">
|
||||
% for section in config['home_sections']:
|
||||
% if section == 'current_activity':
|
||||
@@ -22,7 +23,6 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div id="currentActivity">
|
||||
<% from plexpy import PLEX_SERVER_UP %>
|
||||
% if PLEX_SERVER_UP:
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
@@ -135,7 +135,17 @@
|
||||
<div class="row">
|
||||
<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>
|
||||
% 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>
|
||||
% endif
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,6 +232,7 @@
|
||||
</%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>
|
||||
@@ -254,7 +265,6 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<% from plexpy import PLEX_SERVER_UP %>
|
||||
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
|
||||
<script>
|
||||
var defaultHandler = {
|
||||
@@ -380,8 +390,8 @@
|
||||
$('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true)');
|
||||
$('#metadata-grandparent_title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
||||
.attr('title', s.grandparent_title)
|
||||
.text(s.grandparent_title);
|
||||
.attr('title', s.original_title || s.grandparent_title)
|
||||
.text(s.original_title || s.grandparent_title);
|
||||
}
|
||||
// Update cover if album changed
|
||||
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
|
||||
@@ -396,7 +406,11 @@
|
||||
.text(s.parent_title);
|
||||
}
|
||||
// Update cover if track changed
|
||||
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
|
||||
if (s.rating_key !== instance.data('rating_key')) {
|
||||
$('#metadata-grandparent_title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
||||
.attr('title', s.original_title || s.grandparent_title)
|
||||
.text(s.original_title || s.grandparent_title);
|
||||
$('#metadata-title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.rating_key)
|
||||
.attr('title', s.title)
|
||||
@@ -746,7 +760,7 @@
|
||||
getLibraryStats();
|
||||
</script>
|
||||
% endif
|
||||
% if 'recently_added' in config['home_sections']:
|
||||
% if 'recently_added' in config['home_sections'] and PLEX_SERVER_UP:
|
||||
<script>
|
||||
function recentlyAdded(recently_added_count, recently_added_type) {
|
||||
showMsg("Loading recently added items...", true, false, 0);
|
||||
|
@@ -165,7 +165,7 @@ DOCUMENTATION :: END
|
||||
<h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||
<h2>${data['title']}</h2>
|
||||
% elif data['media_type'] == 'track':
|
||||
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
|
||||
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['original_title'] or data['grandparent_title']}</a></h1>
|
||||
<h2><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2>
|
||||
<h3 class="hidden-xs">T${data['media_index']}</h3>
|
||||
% endif
|
||||
@@ -371,7 +371,11 @@ DOCUMENTATION :: END
|
||||
<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':
|
||||
@@ -502,7 +506,7 @@ DOCUMENTATION :: END
|
||||
% elif data['media_type'] == 'album':
|
||||
${data['parent_title']}<br />${data['title']}
|
||||
% elif data['media_type'] == 'track':
|
||||
${data['grandparent_title']}<br />${data['title']}<br />${data['parent_title']}
|
||||
${data['original_title'] or data['grandparent_title']}<br />${data['title']}<br />${data['parent_title']}
|
||||
% endif
|
||||
</strong>
|
||||
</p>
|
||||
|
@@ -91,7 +91,7 @@ DOCUMENTATION :: END
|
||||
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
Episode ${child['media_index']}
|
||||
Episode ${child['media_index'] or child['originally_available_at']}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,16 +122,24 @@ DOCUMENTATION :: END
|
||||
% elif data['children_type'] == 'track':
|
||||
% if loop.index % 2 == 0:
|
||||
<div class="item-children-list-item-even">
|
||||
<span class="item-children-list-item-index">${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span>
|
||||
<span class="item-children-list-item-index"> ${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
% if child['original_title']:
|
||||
<span class="text-muted"> - ${child['original_title']}</span>
|
||||
% 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>
|
||||
</span>
|
||||
</div>
|
||||
% else:
|
||||
<div class="item-children-list-item-odd">
|
||||
<span class="item-children-list-item-index">${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span>
|
||||
<span class="item-children-list-item-index"> ${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
% if child['original_title']:
|
||||
<span class="text-muted"> - ${child['original_title']}</span>
|
||||
% 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>
|
||||
</span>
|
||||
|
@@ -251,7 +251,7 @@ DOCUMENTATION :: END
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
<div class="item-children-instance-text-wrapper album-item">
|
||||
<h3 title="${child['grandparent_title']}">${child['grandparent_title']}</h3>
|
||||
<h3 title="${child['original_title'] or child['grandparent_title']}">${child['original_title'] or child['grandparent_title']}</h3>
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
<h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3>
|
||||
</div>
|
||||
|
@@ -1,17 +1,19 @@
|
||||
function initConfigCheckbox(elem) {
|
||||
var config = $(elem).closest('div').next();
|
||||
function initConfigCheckbox(elem, toggleElem, reverse) {
|
||||
toggleElem = (toggleElem === undefined) ? null : toggleElem;
|
||||
reverse = (reverse === undefined) ? false : reverse;
|
||||
var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
|
||||
config.css('overflow', 'hidden');
|
||||
if ($(elem).is(":checked")) {
|
||||
config.show();
|
||||
config.toggle(!reverse);
|
||||
} else {
|
||||
config.hide();
|
||||
config.toggle(reverse);
|
||||
}
|
||||
$(elem).click(function () {
|
||||
var config = $(this).closest('div').next();
|
||||
var config = toggleElem ? $(toggleElem) : $(this).closest('div').next();
|
||||
if ($(this).is(":checked")) {
|
||||
config.slideDown();
|
||||
config.slideToggleBool(!reverse);
|
||||
} else {
|
||||
config.slideUp();
|
||||
config.slideToggleBool(reverse);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -36,7 +38,7 @@ function showMsg(msg, loader, timeout, ms, error) {
|
||||
var message = $("<div class='msg'>" + msg + "</div>");
|
||||
if (loader) {
|
||||
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding", "14px 10px")
|
||||
feedback.css("padding", "14px 10px");
|
||||
}
|
||||
if (error) {
|
||||
feedback.css("background-color", "rgba(255,0,0,0.5)");
|
||||
@@ -59,7 +61,7 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
|
||||
$('#confirm-modal').modal();
|
||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||
if (loader_msg) {
|
||||
showMsg(loader_msg, true, false)
|
||||
showMsg(loader_msg, true, false);
|
||||
}
|
||||
$.ajax({
|
||||
url: url,
|
||||
@@ -71,9 +73,9 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||
} else {
|
||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true);
|
||||
}
|
||||
if (typeof callback === "function") {
|
||||
callback(result);
|
||||
@@ -85,8 +87,8 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
|
||||
|
||||
function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
// Set Message
|
||||
feedback = (showMsg) ? $("#ajaxMsg") : $();
|
||||
update = $("#updatebar");
|
||||
var feedback = (showMsg) ? $("#ajaxMsg") : $();
|
||||
var update = $("#updatebar");
|
||||
if (update.is(":visible")) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom", height + "px");
|
||||
@@ -96,8 +98,9 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
feedback.fadeIn();
|
||||
// Get Form data
|
||||
var formID = "#" + url;
|
||||
if (form == true) {
|
||||
var dataString = $(formID).serialize();
|
||||
var dataString;
|
||||
if (form === true) {
|
||||
dataString = $(formID).serialize();
|
||||
}
|
||||
// Loader Image
|
||||
var loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
@@ -105,13 +108,13 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
var dataSucces = $(elem).data('success');
|
||||
if (typeof dataSucces === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
var dataSucces = "Success!";
|
||||
dataSucces = "Success!";
|
||||
}
|
||||
// Data Errror Message
|
||||
var dataError = $(elem).data('error');
|
||||
if (typeof dataError === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
var dataError = "There was an error";
|
||||
dataError = "There was an error";
|
||||
}
|
||||
// Get Success & Error message from inline data, else use standard message
|
||||
var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>");
|
||||
@@ -120,7 +123,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
if (form) {
|
||||
if ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ||
|
||||
$('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) {
|
||||
feedback.addClass('error')
|
||||
feedback.addClass('error');
|
||||
$(feedback).prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(function () {
|
||||
@@ -128,7 +131,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
feedback.fadeOut(function () {
|
||||
feedback.removeClass('error');
|
||||
});
|
||||
})
|
||||
});
|
||||
$(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
|
||||
}, 2000);
|
||||
return false;
|
||||
@@ -144,33 +147,33 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
feedback.prepend(loader);
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
feedback.addClass('error')
|
||||
feedback.addClass('error');
|
||||
feedback.prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(function () {
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function () {
|
||||
feedback.removeClass('error')
|
||||
feedback.removeClass('error');
|
||||
});
|
||||
})
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
success: function (data, jqXHR) {
|
||||
feedback.prepend(succesMsg);
|
||||
feedback.addClass('success')
|
||||
feedback.addClass('success');
|
||||
setTimeout(function (e) {
|
||||
succesMsg.fadeOut(function () {
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function () {
|
||||
feedback.removeClass('success');
|
||||
});
|
||||
if (reload == true) refreshSubmenu();
|
||||
if (reload == "table") {
|
||||
if (reload === true) refreshSubmenu();
|
||||
if (reload === "table") {
|
||||
refreshTable();
|
||||
}
|
||||
if (reload == "tabs") refreshTab();
|
||||
if (reload == "page") location.reload();
|
||||
if (reload == "submenu&table") {
|
||||
if (reload === "tabs") refreshTab();
|
||||
if (reload === "page") location.reload();
|
||||
if (reload === "submenu&table") {
|
||||
refreshSubmenu();
|
||||
refreshTable();
|
||||
}
|
||||
@@ -179,7 +182,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
$(formID + " select").children('option[disabled=disabled]').attr(
|
||||
'selected', 'selected');
|
||||
}
|
||||
})
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
complete: function (jqXHR, textStatus) {
|
||||
@@ -215,19 +218,20 @@ function isPrivateIP(ip_address) {
|
||||
|
||||
$.cachedScript('js/ipaddr.min.js').done(function () {
|
||||
if (ipaddr.isValid(ip_address)) {
|
||||
var addr = ipaddr.process(ip_address)
|
||||
var addr = ipaddr.process(ip_address);
|
||||
|
||||
var rangeList = [];
|
||||
if (addr.kind() === 'ipv4') {
|
||||
var rangeList = [
|
||||
rangeList = [
|
||||
ipaddr.parseCIDR('127.0.0.0/8'),
|
||||
ipaddr.parseCIDR('10.0.0.0/8'),
|
||||
ipaddr.parseCIDR('172.16.0.0/12'),
|
||||
ipaddr.parseCIDR('192.168.0.0/16')
|
||||
]
|
||||
];
|
||||
} else {
|
||||
var rangeList = [
|
||||
rangeList = [
|
||||
ipaddr.parseCIDR('fd00::/8')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
|
||||
@@ -238,12 +242,13 @@ function isPrivateIP(ip_address) {
|
||||
} else {
|
||||
defer.resolve('n/a');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return defer.promise();
|
||||
}
|
||||
|
||||
function humanTime(seconds) {
|
||||
var text;
|
||||
if (seconds >= 86400) {
|
||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' + '<h3>' +
|
||||
Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' + '<h3>' +
|
||||
@@ -265,6 +270,7 @@ 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(
|
||||
@@ -341,7 +347,7 @@ function getCookie(cname) {
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') c = c.substring(1);
|
||||
if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
|
||||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -354,24 +360,24 @@ var Accordion = function (el, multiple) {
|
||||
links.on('click', {
|
||||
el: this.el,
|
||||
multiple: this.multiple
|
||||
}, this.dropdown)
|
||||
}
|
||||
}, this.dropdown);
|
||||
};
|
||||
Accordion.prototype.dropdown = function (e) {
|
||||
var $el = e.data.el;
|
||||
$this = $(this),
|
||||
$next = $this.next();
|
||||
$this = $(this);
|
||||
$next = $this.next();
|
||||
$next.slideToggle();
|
||||
$this.parent().toggleClass('open');
|
||||
if (!e.data.multiple) {
|
||||
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function clearSearchButton(tableName, table) {
|
||||
$('#' + tableName + '_filter').find('input[type=search]').wrap(
|
||||
'<div class="input-group" role="group" aria-label="Search"></div>').after(
|
||||
'<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' +
|
||||
tableName + '"><i class="fa fa-remove"></i></button></span>')
|
||||
tableName + '"><i class="fa fa-remove"></i></button></span>');
|
||||
$('#clear-search-' + tableName).click(function () {
|
||||
table.search('').draw();
|
||||
});
|
||||
@@ -401,7 +407,6 @@ $('*').on('click', '.refresh_pms_image', function (e) {
|
||||
} else {
|
||||
if (pms_proxy_url.indexOf('refresh=true') > -1) {
|
||||
pms_proxy_url = pms_proxy_url.replace("&refresh=true", "");
|
||||
console.log(pms_proxy_url)
|
||||
background_div.css('background-image', 'url(' + pms_proxy_url + ')');
|
||||
background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)');
|
||||
} else {
|
||||
@@ -416,8 +421,7 @@ function humanFileSize(bytes, si) {
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
var units = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
var u = -1;
|
||||
do {
|
||||
@@ -436,10 +440,10 @@ function forceMinMax(elem) {
|
||||
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 {
|
||||
@@ -449,4 +453,8 @@ function forceMinMax(elem) {
|
||||
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
$.fn.slideToggleBool = function(bool, options) {
|
||||
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
|
||||
};
|
@@ -11,12 +11,11 @@ DOCUMENTATION :: END
|
||||
|
||||
<ul class="stacked-configs list-unstyled">
|
||||
% for device in sorted(devices_list, key=lambda k: k['device_name']):
|
||||
<li class="mobile-device" data-id="${device['id']}" data-name="${device['device_name']}">
|
||||
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
|
||||
<span>
|
||||
<!--<span class="toggle-right mobile-device-tooltip edit-mobile-device" data-toggle="tooltip" data-placement="top" title="Edit Device"><i class="fa fa-lg fa-pencil"></i></span>-->
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
|
||||
${device['friendly_name'] or device['device_name']} <span class="friendly_name">(${device['id']})</span>
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span>
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
|
||||
<span class="toggle-right friendly_name" id="device-last_seen-${device['id']}">
|
||||
% if device['last_seen']:
|
||||
<script>
|
||||
@@ -26,14 +25,13 @@ DOCUMENTATION :: END
|
||||
never
|
||||
% endif
|
||||
</span>
|
||||
<!--<span class="toggle-right delete-mobile-device" data-toggle="tooltip" data-placement="top" title="Remove Device"><i class="fa fa-lg fa-times"></i></span>-->
|
||||
</span>
|
||||
</li>
|
||||
% endfor
|
||||
<li class="add-mobile-device" id="register-mobile-device" data-target="#api-qr-modal" data-toggle="modal">
|
||||
<li class="add-mobile-device pointer" id="register-mobile-device" data-target="#api-qr-modal" data-toggle="modal">
|
||||
<span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span> Register a new device
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span> Register a new device
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
43
data/interfaces/default/newsletter_auth.html
Normal file
43
data/interfaces/default/newsletter_auth.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<%
|
||||
import urllib
|
||||
%>
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="body-container">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="login-container">
|
||||
<div class="newsletter-logo">
|
||||
<img src="${http_root}images/newsletter/newsletter-header.png" height="100" alt="PlexPy">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<form action="${uri}" method="post" id="newsletter-form">
|
||||
<div class="form-group">
|
||||
<label for="password" class="control-label">
|
||||
Password
|
||||
</label>
|
||||
<input type="password" id="key" name="key" class="form-control" autofocus>
|
||||
</div>
|
||||
<button id="enter" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Enter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -20,7 +20,7 @@
|
||||
<div class="row">
|
||||
<ul class="nav nav-tabs list-unstyled" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#tabs-newsletter_config" aria-controls="tabs-newsletter_config" role="tab" data-toggle="tab">Configuration</a></li>
|
||||
<li role="presentation"><a href="#tabs-newsletter_agent" aria-controls="tabs-newsletter_agent" role="tab" data-toggle="tab">Notification Agent</a></li>
|
||||
<li role="presentation"><a href="#tabs-newsletter_saving_sending" aria-controls="tabs-newsletter_saving_sending" role="tab" data-toggle="tab">Saving & Sending</a></li>
|
||||
<li role="presentation"><a href="#tabs-newsletter_text" aria-controls="tabs-newsletter_text" role="tab" data-toggle="tab">Newsletter Text</a></li>
|
||||
<li role="presentation"><a href="#tabs-test_newsletter" aria-controls="tabs-test_newsletter" role="tab" data-toggle="tab">Test Newsletter</a></li>
|
||||
</ul>
|
||||
@@ -70,7 +70,7 @@
|
||||
<p class="help-block">Set the time frame to include in the newsletter. Note: Days uses calendar days (i.e. since midnight).</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" style="padding-top: 10px; border-top: 1px solid #444;">
|
||||
<div class="col-md-12 modal-config-section">
|
||||
<input type="hidden" id="newsletter_id" name="newsletter_id" value="${newsletter['id']}" />
|
||||
<input type="hidden" id="agent_id" name="agent_id" value="${newsletter['agent_id']}" />
|
||||
% for item in newsletter['config_options']:
|
||||
@@ -165,7 +165,16 @@
|
||||
% endif
|
||||
% endfor
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #444;">
|
||||
<div class="col-md-12 modal-config-section">
|
||||
<div class="form-group">
|
||||
<label for="id_name">Unique ID Name</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="text" class="form-control" id="id_name" name="id_name" value="${newsletter['id_name']}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Optional: Enter a unique ID name to create a static URL to the last sent scheduled newsletter at <span class="inline-pre">${http_root}newsletter/id/<id_name></span>. Only letters (a-z), numbers (0-9), underscores (_) and hyphens (-) are allowed. Leave blank to disable.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="friendly_name">Description</label>
|
||||
<div class="row">
|
||||
@@ -178,12 +187,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_agent">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_saving_sending">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label>Saving</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="newsletter_config_formatted_checkbox" data-id="newsletter_config_formatted" class="checkboxes" value="1" ${checked(newsletter['config']['formatted'])}> Send newsletter as an HTML formatted Email
|
||||
<input type="checkbox" id="newsletter_config_save_only_checkbox" data-id="newsletter_config_save_only" class="checkboxes" value="1" ${checked(newsletter['config']['save_only'])}> Save HTML File Only
|
||||
</label>
|
||||
<p class="help-block">Enable to save the newsletter HTML file without sending it to any notification agent.</p>
|
||||
<input type="hidden" id="newsletter_config_save_only" name="newsletter_config_save_only" value="${newsletter['config']['save_only']}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newsletter_config_filename">HTML File Name</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="text" class="form-control" id="newsletter_config_filename" name="newsletter_config_filename" value="${newsletter['config']['filename']}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Optional: Enter the file name to use when saving the newsletter (ending with <span class="inline-pre">.html</span>). You may use any of the <a href="#newsletter-text-sub-modal" data-toggle="modal">newsletter text parameters</a>. Leave blank for default.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 modal-config-section" id="newsletter_agent_options">
|
||||
<label>Sending</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="newsletter_config_formatted_checkbox" data-id="newsletter_config_formatted" class="checkboxes" value="1" ${checked(newsletter['config']['formatted'])}> Send Newsletter as an HTML Formatted Email
|
||||
</label>
|
||||
<p class="help-block">Enable to send the newsletter as an HTML formatted Email. Disable to only send a subject and body message to a different notification agent.</p>
|
||||
<input type="hidden" id="newsletter_config_formatted" name="newsletter_config_formatted" value="${newsletter['config']['formatted']}">
|
||||
@@ -234,100 +263,100 @@
|
||||
Note: Self-hosted newsletters must be enabled under <a data-tab-destination="tabs-notifications" data-dismiss="modal" data-target="#newsletter_self_hosted">Newsletters</a> to include a link to the newsletter.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newsletter-email-config" class="col-md-12" style="padding-top: 10px; border-top: 1px solid #444;">
|
||||
% for item in newsletter['email_config_options']:
|
||||
% if item['input_type'] == 'help':
|
||||
<div class="form-group">
|
||||
<label>${item['label']}</label>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'text' or item['input_type'] == 'password':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<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 ''}>
|
||||
</div>
|
||||
<div id="newsletter-email-config">
|
||||
% for item in newsletter['email_config_options']:
|
||||
% if item['input_type'] == 'help':
|
||||
<div class="form-group">
|
||||
<label>${item['label']}</label>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'number':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
|
||||
% elif item['input_type'] == 'text' or item['input_type'] == 'password':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<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 ''}>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'button':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||
% elif item['input_type'] == 'number':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'checkbox' and item['name'] != 'newsletter_email_html_support':
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
|
||||
</label>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||
</div>
|
||||
% elif item['input_type'] == 'select':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||
% for key, value in sorted(item['select_options'].iteritems()):
|
||||
% if key == item['value']:
|
||||
<option value="${key}" selected>${value}</option>
|
||||
% else:
|
||||
<option value="${key}">${value}</option>
|
||||
% endif
|
||||
% endfor
|
||||
</select>
|
||||
% elif item['input_type'] == 'button':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'selectize':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||
<option value="select-all">Select All</option>
|
||||
<option value="remove-all">Remove All</option>
|
||||
% if isinstance(item['select_options'], dict):
|
||||
% for section, options in item['select_options'].iteritems():
|
||||
<optgroup label="${section}">
|
||||
% for option in sorted(options, key=lambda x: x['text'].lower()):
|
||||
% elif item['input_type'] == 'checkbox' and item['name'] != 'newsletter_email_html_support':
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
|
||||
</label>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||
</div>
|
||||
% elif item['input_type'] == 'select':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||
% for key, value in sorted(item['select_options'].iteritems()):
|
||||
% if key == item['value']:
|
||||
<option value="${key}" selected>${value}</option>
|
||||
% else:
|
||||
<option value="${key}">${value}</option>
|
||||
% endif
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'selectize':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||
<option value="select-all">Select All</option>
|
||||
<option value="remove-all">Remove All</option>
|
||||
% if isinstance(item['select_options'], dict):
|
||||
% for section, options in item['select_options'].iteritems():
|
||||
<optgroup label="${section}">
|
||||
% for option in sorted(options, key=lambda x: x['text'].lower()):
|
||||
<option value="${option['value']}">${option['text']}</option>
|
||||
% endfor
|
||||
</optgroup>
|
||||
% endfor
|
||||
% else:
|
||||
<option value="border-all"></option>
|
||||
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
|
||||
<option value="${option['value']}">${option['text']}</option>
|
||||
% endfor
|
||||
</optgroup>
|
||||
% endfor
|
||||
% else:
|
||||
<option value="border-all"></option>
|
||||
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
|
||||
<option value="${option['value']}">${option['text']}</option>
|
||||
% endfor
|
||||
% endif
|
||||
</select>
|
||||
% endif
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
% endif
|
||||
% endfor
|
||||
<input type="hidden" id="newsletter_email_html_support" name="newsletter_email_html_support" value="1">
|
||||
</div>
|
||||
% endif
|
||||
% endfor
|
||||
<input type="hidden" id="newsletter_email_html_support" name="newsletter_email_html_support" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -458,6 +487,26 @@
|
||||
toggleCustomCron();
|
||||
});
|
||||
|
||||
function validateFilename() {
|
||||
var filename = $('#newsletter_config_filename').val();
|
||||
if (filename !== '' && !(filename.endsWith('.html'))) {
|
||||
showMsg('<i class="fa fa-times"></i> Failed to save newsletter. Invalid file name.', false, true, 5000, true);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function validateIDName() {
|
||||
var id_name = $('#id_name').val();
|
||||
if (/^[a-zA-Z0-9_-]*$/.test(id_name)) {
|
||||
return true;
|
||||
} else {
|
||||
showMsg('<i class="fa fa-times"></i> Failed to save newsletter. Invalid unique ID name.', false, true, 5000, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var $incl_libraries = $('#newsletter_config_incl_libraries').selectize({
|
||||
plugins: ['remove_button'],
|
||||
maxItems: null,
|
||||
@@ -485,6 +534,8 @@
|
||||
var incl_libraries = $incl_libraries[0].selectize;
|
||||
incl_libraries.setValue(${json.dumps(next((c['value'] for c in newsletter['config_options'] if c['name'] == 'newsletter_config_incl_libraries'), [])) | n});
|
||||
|
||||
initConfigCheckbox('#newsletter_config_save_only_checkbox', '#newsletter_agent_options', true);
|
||||
|
||||
function toggleEmailSelect () {
|
||||
if ($('#newsletter_config_formatted_checkbox').is(':checked')) {
|
||||
$('#newsletter_body').hide();
|
||||
@@ -643,7 +694,9 @@
|
||||
if ($('#custom_cron').val() === '0'){
|
||||
$("#cron_value").val(cron_widget.cron('value'));
|
||||
}
|
||||
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, true, saveCallback);
|
||||
if (validateFilename() && validateIDName()){
|
||||
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, true, saveCallback);
|
||||
}
|
||||
}
|
||||
|
||||
$('#delete-newsletter-item').click(function () {
|
||||
|
@@ -32,7 +32,7 @@
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var frame = $('<iframe></iframe>', {
|
||||
src: '${http_root}real_newsletter?${urllib.urlencode(kwargs) | n}',
|
||||
src: 'real_newsletter?${urllib.urlencode(kwargs) | n}',
|
||||
frameborder: '0',
|
||||
style: 'display: none; height: 100vh; width: 100vw;'
|
||||
});
|
||||
|
@@ -12,15 +12,15 @@ DOCUMENTATION :: END
|
||||
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %>
|
||||
<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" data-id="${newsletter['id']}">
|
||||
<li class="newsletter-agent pointer" data-id="${newsletter['id']}">
|
||||
<span>
|
||||
<span class="toggle-left trigger-tooltip ${'active' if newsletter['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Newsletter ${'active' if newsletter['active'] else 'inactive'}"><i class="fa fa-lg fa-newspaper-o"></i></span>
|
||||
<span class="toggle-left trigger-tooltip ${'active' if newsletter['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Newsletter ${'active' if newsletter['active'] else 'inactive'}"><i class="fa fa-lg fa-fw fa-newspaper-o"></i></span>
|
||||
% if newsletter['friendly_name']:
|
||||
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
|
||||
% else:
|
||||
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']})</span>
|
||||
% endif
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span>
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
|
||||
<span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}">
|
||||
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
|
||||
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
|
||||
@@ -32,10 +32,10 @@ DOCUMENTATION :: END
|
||||
</span>
|
||||
</li>
|
||||
% endfor
|
||||
<li class="add-newsletter-agent" id="add-newsletter-agent" data-target="#add-newsletter-modal" data-toggle="modal">
|
||||
<li class="add-newsletter-agent pointer" id="add-newsletter-agent" data-target="#add-newsletter-modal" data-toggle="modal">
|
||||
<span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-newspaper-o"></i></span> Add a new newsletter agent
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-newspaper-o"></i></span> Add a new newsletter agent
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -1,7 +1,8 @@
|
||||
% if notifier:
|
||||
<%!
|
||||
import json
|
||||
from plexpy import helpers, notifiers, users
|
||||
from plexpy import notifiers, users
|
||||
from plexpy.helpers import checked
|
||||
available_notification_actions = notifiers.available_notification_actions()
|
||||
|
||||
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
|
||||
@@ -70,7 +71,7 @@
|
||||
% elif item['input_type'] == 'checkbox':
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${helpers.checked(item['value'])}> ${item['label']}
|
||||
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
|
||||
</label>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||
@@ -123,7 +124,7 @@
|
||||
% endif
|
||||
% endfor
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #444;">
|
||||
<div class="col-md-12 modal-config-section">
|
||||
<div class="form-group">
|
||||
<label for="friendly_name">Description</label>
|
||||
<div class="row">
|
||||
@@ -146,7 +147,7 @@
|
||||
% for action in available_notification_actions:
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${helpers.checked(notifier['actions'][action['name']])}> ${action['label']}
|
||||
<input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${checked(notifier['actions'][action['name']])}> ${action['label']}
|
||||
</label>
|
||||
<p class="help-block">${action['description'] | n}</p>
|
||||
<input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}">
|
||||
|
@@ -11,22 +11,22 @@ DOCUMENTATION :: END
|
||||
|
||||
<ul class="stacked-configs list-unstyled">
|
||||
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])):
|
||||
<li class="notification-agent" data-id="${notifier['id']}">
|
||||
<li class="notification-agent pointer" data-id="${notifier['id']}">
|
||||
<span>
|
||||
<span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-bell"></i></span>
|
||||
<span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-fw fa-bell"></i></span>
|
||||
% if notifier['friendly_name']:
|
||||
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
|
||||
% else:
|
||||
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']})</span>
|
||||
% endif
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span>
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
|
||||
</span>
|
||||
</li>
|
||||
% endfor
|
||||
<li class="add-notification-agent" id="add-notification-agent" data-target="#add-notifier-modal" data-toggle="modal">
|
||||
<li class="add-notification-agent pointer" id="add-notification-agent" data-target="#add-notifier-modal" data-toggle="modal">
|
||||
<span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-bell"></i></span> Add a new notification agent
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span>
|
||||
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-bell"></i></span> Add a new notification agent
|
||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -28,15 +28,17 @@
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script>
|
||||
var query_string = "${query.replace('"','\\"').replace('/','\\/') | n}";
|
||||
|
||||
$('#search_button').removeClass('btn-inactive');
|
||||
$('#query').val("${query.replace('"','\\"') | n}").css({ right: '0', width: '250px' }).addClass('active');
|
||||
$('#query').val(query_string).css({ right: '0', width: '250px' }).addClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: 'get_search_results_children',
|
||||
type: "GET",
|
||||
type: "POST",
|
||||
async: true,
|
||||
data: {
|
||||
query: "${query.replace('"','\\"') | n}",
|
||||
query: query_string,
|
||||
limit: 30
|
||||
},
|
||||
complete: function (xhr, status) {
|
||||
|
@@ -650,7 +650,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group has-feedback" id="pms_ip_group">
|
||||
<label for="pms_ip">Plex IP or Hostname</label>
|
||||
<label for="pms_ip">Plex IP Address or Hostname</label>
|
||||
<div class="row">
|
||||
<div class="col-md-9" id="selectize-pms-ip-container">
|
||||
<div class="input-group">
|
||||
@@ -702,6 +702,17 @@
|
||||
The server URL that Tautulli will use to connect to your Plex server. Retrieved automatically.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label for="pms_url">Plex Server Identifier</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}" size="30" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
The unique identifier for your Plex server. Retrieved automatically.
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
|
||||
@@ -728,7 +739,6 @@
|
||||
</div>
|
||||
|
||||
<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']}">
|
||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
|
||||
|
||||
<div class="form-group advanced-setting">
|
||||
@@ -955,11 +965,63 @@
|
||||
<p class="help-block">Enable to host newsletters on your own domain. This will generate a link to an HTML page where you can view the newsletter.</p>
|
||||
</div>
|
||||
<div id="self_host_newsletter_options" style="overlfow: hidden; display: ${'block' if config['newsletter_self_hosted'] == 'checked' else 'none'}">
|
||||
<p class="help-block" id="self_host_newsletter_message">
|
||||
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.<br>
|
||||
Note: Newsletter images will be self-hosted regardless of the Image Hosting setting below.<br>
|
||||
<div class="form-group">
|
||||
<p class="help-block" id="self_host_newsletter_message">
|
||||
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
|
||||
</p>
|
||||
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newsletter_auth">Newsletter Authentication</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<select class="form-control" id="newsletter_auth" name="newsletter_auth">
|
||||
<option value="0" ${'selected' if config['newsletter_auth'] == 0 else ''}>Disabled</option>
|
||||
<option value="1" ${'selected' if config['newsletter_auth'] == 1 else ''}>Password</option>
|
||||
<option value="2" ${'selected' if config['newsletter_auth'] == 2 else ''}>Tautulli Guest Access</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Select the authentication method to use for self-hosted newsletters.</p>
|
||||
<p class="help-block settings-warning newsletter-guest-access-warning">Warning: Guest Access is not enabled under <a data-tab-destination="tabs-web_interface" data-target="#allow_guest_access">Web Interface</a>.</p>
|
||||
</div>
|
||||
<div class="form-group" id="newsletter_password_option">
|
||||
<label for="newsletter_password">Newsletter Password</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="newsletter_password" name="newsletter_password" value="${config['newsletter_password']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Enter the password that will be required to view self-hosted newsletters.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" id="newsletter_inline_styles" name="newsletter_inline_styles" value="1" ${config['newsletter_inline_styles']}> Use Inline Styles Template
|
||||
</label>
|
||||
<p class="help-block">
|
||||
Enable to use newsletter templates with inline CSS styles. Inline styles render better in email clients, but are larger in size which may cause long newsletters to be clipped.<br>
|
||||
Note: This setting does not affect custom templates. CSS styles will depend on your own template.
|
||||
</p>
|
||||
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label for="newsletter_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>
|
||||
</div>
|
||||
<p class="help-block">Optional: Enter the full path to your custom newsletter templates folder. Leave blank for default.</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label for="newsletter_dir">Newsletter Output Directory</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
@@ -970,21 +1032,30 @@
|
||||
<label for="notify_upload_posters">Image Hosting</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<select class="form-control" id="notify_upload_posters" name="notify_upload_posters">
|
||||
<option value="0" ${'selected' if config['notify_upload_posters'] == 0 else ''}>Disabled</option>
|
||||
<option value="1" ${'selected' if config['notify_upload_posters'] == 1 else ''}>Imgur</option>
|
||||
<option value="3" ${'selected' if config['notify_upload_posters'] == 3 else ''}>Cloudinary</option>
|
||||
<option value="2" ${'selected' if config['notify_upload_posters'] == 2 else ''}>Self-hosted on public Tautulli domain</option>
|
||||
</select>
|
||||
<div class="${'input-group' if config['notify_upload_posters'] in (1, 3) else ''}">
|
||||
<select class="form-control" id="notify_upload_posters" name="notify_upload_posters">
|
||||
<option value="0" ${'selected' if config['notify_upload_posters'] == 0 else ''}>Disabled</option>
|
||||
<option value="1" ${'selected' if config['notify_upload_posters'] == 1 else ''}>Imgur</option>
|
||||
<option value="3" ${'selected' if config['notify_upload_posters'] == 3 else ''}>Cloudinary</option>
|
||||
<option value="2" ${'selected' if config['notify_upload_posters'] == 2 else ''}>Self-hosted on public domain</option>
|
||||
</select>
|
||||
% if config['notify_upload_posters'] in (1, 3):
|
||||
<span class="input-group-btn" id="delete_all_uploads_container">
|
||||
<button class="btn btn-form" type="button" id="delete_all_uploads">Delete All Uploads</button>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Select where to host Plex images for notifications and newsletters.</p>
|
||||
</div>
|
||||
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
||||
<p class="help-block" id="imgur_upload_message">
|
||||
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
|
||||
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<p class="help-block" id="imgur_upload_message">
|
||||
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
|
||||
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imgur_client_id">Imgur Client ID</label>
|
||||
<div class="row">
|
||||
@@ -996,13 +1067,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
|
||||
<p class="help-block" id="self_host_image_message">Note: The <span class="inline-pre">${http_root}image</span> endpoint on your domain must be publicly accessible from the internet.</p>
|
||||
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
|
||||
<div class="form-group">
|
||||
<p class="help-block" id="self_host_image_message">Note: The <span class="inline-pre">${http_root}image</span> endpoint on your domain must be publicly accessible from the internet.</p>
|
||||
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
||||
<p class="help-block" id="imgur_upload_message">
|
||||
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<p class="help-block" id="imgur_upload_message">
|
||||
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cloudinary_cloud_name">Cloudinary Cloud Name</label>
|
||||
<div class="row">
|
||||
@@ -1084,7 +1159,7 @@
|
||||
Add a new newsletter agent, or configure an existing newsletter agent by clicking the settings icon on the right.
|
||||
</p>
|
||||
<p class="help-block settings-warning" id="newsletter_upload_warning">
|
||||
Note: Either <a data-tab-destination="tabs-notifications" data-target="#notify_upload_posters">Image Hosting</a> or <a data-tab-destination="tabs-notifications" data-target="#newsletter_self_hosted">Self-Hosted Newsletters</a> must be enabled.</span>
|
||||
Warning: The <a data-tab-destination="tabs-notifications" data-target="#notify_upload_posters">Image Hosting</a> setting must be enabled for images to display on the newsletter.</span>
|
||||
</p>
|
||||
<br/>
|
||||
<div id="plexpy-newsletters-table">
|
||||
@@ -1173,14 +1248,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newsletter_dir">Newsletter Directory</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control directory-settings" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||
|
||||
@@ -1201,7 +1268,7 @@
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="form-group">
|
||||
<div class="form-group">
|
||||
<label>Registered Devices</label>
|
||||
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
|
||||
<p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-web_interface" data-target="#api_enabled">Web Interface</a> to use the app.</p>
|
||||
@@ -1356,7 +1423,7 @@
|
||||
<div class="col-md-12">
|
||||
<ul class="stacked-configs list-unstyled">
|
||||
% for agent in sorted(available_notification_agents, key=lambda k: k['label'].lower()):
|
||||
<li class="new-notification-agent" data-id="${agent['id']}">
|
||||
<li class="new-notification-agent pointer" data-id="${agent['id']}">
|
||||
<span>${agent['label']}</span>
|
||||
</li>
|
||||
% endfor
|
||||
@@ -1384,7 +1451,7 @@
|
||||
<div class="col-md-12">
|
||||
<ul class="stacked-configs list-unstyled">
|
||||
% for agent in available_newsletter_agents:
|
||||
<li class="new-newsletter-agent" data-id="${agent['id']}">
|
||||
<li class="new-newsletter-agent pointer" data-id="${agent['id']}">
|
||||
<span>${agent['label']}</span>
|
||||
</li>
|
||||
% endfor
|
||||
@@ -1726,6 +1793,7 @@
|
||||
}
|
||||
|
||||
function loadNotifierConfig(notifier_id) {
|
||||
showMsg('<i class="fa fa-refresh fa-spin"></i> Loading Configuration', false);
|
||||
$.ajax({
|
||||
url: 'get_notifier_config_modal',
|
||||
data: { notifier_id: notifier_id },
|
||||
@@ -1733,6 +1801,7 @@
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
$("#notifier-config-modal").html(xhr.responseText).modal('show');
|
||||
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1749,6 +1818,7 @@
|
||||
}
|
||||
|
||||
function loadNewsletterConfig(newsletter_id) {
|
||||
showMsg('<i class="fa fa-refresh fa-spin"></i> Loading Configuration', false);
|
||||
$.ajax({
|
||||
url: 'get_newsletter_config_modal',
|
||||
data: { newsletter_id: newsletter_id },
|
||||
@@ -1756,6 +1826,7 @@
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
$("#newsletter-config-modal").html(xhr.responseText).modal('show');
|
||||
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1772,6 +1843,7 @@
|
||||
}
|
||||
|
||||
function loadMobileDeviceConfig(mobile_device_id) {
|
||||
showMsg('<i class="fa fa-refresh fa-spin"></i> Loading Configuration', false);
|
||||
$.ajax({
|
||||
url: 'get_mobile_device_config_modal',
|
||||
data: { mobile_device_id: mobile_device_id },
|
||||
@@ -1779,6 +1851,7 @@
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
$("#mobile-device-config-modal").html(xhr.responseText).modal('show');
|
||||
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2420,6 +2493,7 @@ $(document).ready(function() {
|
||||
$("#allow_guest_access").attr("disabled", false);
|
||||
$("#allowGuestCheck").html("");
|
||||
}
|
||||
newsletterPasswordEnabled();
|
||||
}
|
||||
allowGuestAccessCheck();
|
||||
|
||||
@@ -2523,7 +2597,7 @@ $(document).ready(function() {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
$('#add-notifier-modal').modal('hide');
|
||||
if (result.result == 'success') {
|
||||
if (result.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||
loadNotifierConfig(result.notifier_id);
|
||||
} else {
|
||||
@@ -2545,7 +2619,7 @@ $(document).ready(function() {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
$('#add-newsletter-modal').modal('hide');
|
||||
if (result.result == 'success') {
|
||||
if (result.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||
loadNewsletterConfig(result.newsletter_id);
|
||||
} else {
|
||||
@@ -2586,11 +2660,37 @@ $(document).ready(function() {
|
||||
} else {
|
||||
$('#cloudinary_upload_options').slideUp();
|
||||
}
|
||||
var parent;
|
||||
if (upload_val === '1' || upload_val === '3') {
|
||||
parent = $('#notify_upload_posters').parent();
|
||||
if ($('#delete_all_uploads_container').length === 0){
|
||||
parent.addClass('input-group');
|
||||
parent.append(
|
||||
'<span class="input-group-btn" id="delete_all_uploads_container">' +
|
||||
'<button class="btn btn-form" type="button" id="delete_all_uploads">Delete All Uploads</button>' +
|
||||
'</span>');
|
||||
}
|
||||
} else {
|
||||
parent = $('#notify_upload_posters').parent();
|
||||
parent.removeClass('input-group');
|
||||
$('#delete_all_uploads_container').remove();
|
||||
}
|
||||
}
|
||||
$('#notify_upload_posters').change(function () {
|
||||
imageUpload();
|
||||
});
|
||||
|
||||
$('body').on('click', '#delete_all_uploads', function () {
|
||||
var image_hosting_option = $('#notify_upload_posters').find(':selected');
|
||||
var name = image_hosting_option.text();
|
||||
|
||||
var msg = 'Are you sure you want to delete all uploaded images on <strong>' + name + '</strong>?' +
|
||||
'<br />All previous links to the images will no longer work. This cannot be undone!';
|
||||
var url = 'delete_hosted_images';
|
||||
var data = { service: name, delete_all: true };
|
||||
confirmAjaxCall(url, msg, data, false);
|
||||
});
|
||||
|
||||
function baseURLSet() {
|
||||
if ($('#http_base_url').val()) {
|
||||
$('.base-url-warning').hide();
|
||||
@@ -2605,10 +2705,10 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
function newsletterUploadEnabled() {
|
||||
if ($('#notify_upload_posters').val() !== '2' || $('#newsletter_self_hosted').is(':checked')) {
|
||||
$('#newsletter_upload_warning').hide();
|
||||
} else {
|
||||
if ($('#notify_upload_posters').val() === '0') {
|
||||
$('#newsletter_upload_warning').show();
|
||||
} else {
|
||||
$('#newsletter_upload_warning').hide();
|
||||
}
|
||||
}
|
||||
newsletterUploadEnabled();
|
||||
@@ -2618,6 +2718,28 @@ $(document).ready(function() {
|
||||
newsletterUploadEnabled();
|
||||
});
|
||||
|
||||
function newsletterPasswordEnabled() {
|
||||
if ($('#newsletter_auth').val() === '1') {
|
||||
$('#newsletter_password_option').slideDown();
|
||||
} else {
|
||||
$('#newsletter_password_option').slideUp();
|
||||
}
|
||||
if ($('#newsletter_auth').val() === '2' && !($('#allow_guest_access').is(':checked'))) {
|
||||
$('.newsletter-guest-access-warning').show();
|
||||
} else {
|
||||
$('.newsletter-guest-access-warning').hide();
|
||||
}
|
||||
}
|
||||
newsletterPasswordEnabled();
|
||||
|
||||
$('#newsletter_auth').change(function () {
|
||||
newsletterPasswordEnabled();
|
||||
});
|
||||
|
||||
$('#allow_guest_access').click(function () {
|
||||
newsletterPasswordEnabled();
|
||||
})
|
||||
|
||||
$('body').on('click', 'a[data-tab-destination]', function () {
|
||||
var tab = $(this).data('tab-destination');
|
||||
$("a[href=#" + tab + "]").click();
|
||||
|
@@ -46,8 +46,10 @@ DOCUMENTATION :: END
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="info-modal-title">
|
||||
% if data['media_type'] == 'episode' or data['media_type'] == 'track':
|
||||
% if data['media_type'] == 'episode':
|
||||
Stream Info: <strong>${data['grandparent_title']} - ${data['title']} (${user})</strong>
|
||||
% elif data['media_type'] == 'track':
|
||||
Stream Info: <strong>${data['original_title'] or data['grandparent_title']} - ${data['title']} (${user})</strong>
|
||||
% else:
|
||||
Stream Info: <strong>${data['title']} (${user})</strong>
|
||||
% endif
|
||||
|
@@ -188,7 +188,7 @@ DOCUMENTATION :: END
|
||||
},
|
||||
complete: function (xhr, status) {
|
||||
$('#search-results-list').html(xhr.responseText);
|
||||
$('#update_query_title').html(query_string)
|
||||
$('#update_query_title').text(query_string)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -108,8 +108,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['grandparent_title']}">
|
||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
<h3 title="${item['original_title'] or item['grandparent_title']}">
|
||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['original_title'] or item['grandparent_title']}">${item['original_title'] or item['grandparent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted" title="${item['title']}">
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
|
@@ -1,17 +1,24 @@
|
||||
% if data:
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy.helpers import grouper
|
||||
from plexpy.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/'
|
||||
base_url_image = base_url + 'image/'
|
||||
elif preview:
|
||||
base_url = 'newsletter/'
|
||||
base_url_image = base_url + 'image/'
|
||||
else:
|
||||
base_url = base_url_image = ''
|
||||
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/'
|
||||
elif preview and service and service != 'self-hosted':
|
||||
base_url_image = 'image/'
|
||||
else:
|
||||
base_url_image = ''
|
||||
|
||||
%>
|
||||
<!doctype html>
|
||||
<html>
|
||||
@@ -61,17 +68,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 1042px;
|
||||
padding: 10px;
|
||||
width: 1042px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
@@ -83,6 +87,14 @@
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.local-preview-note {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.local-preview-note p {
|
||||
color: #282A2D;
|
||||
font-size: 12px;
|
||||
}
|
||||
.main {
|
||||
background: #282A2D;
|
||||
border-radius: 3px;
|
||||
@@ -157,88 +169,9 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.mb5 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@@ -248,9 +181,6 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
@@ -309,6 +239,7 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
MEDIA SECTIONS
|
||||
------------------------------------- */
|
||||
@@ -363,6 +294,7 @@
|
||||
font-size: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
MEDIA CARDS
|
||||
------------------------------------- */
|
||||
@@ -465,21 +397,21 @@
|
||||
padding-left: 5px;
|
||||
}
|
||||
.card-info-footer .badge-container {
|
||||
max-width: 265px;
|
||||
max-width: 260px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.card-info-footer .star-rating-container {
|
||||
width: 60px;
|
||||
vertical-align: bottom;
|
||||
padding-right: 5px;
|
||||
width: 65px;
|
||||
}
|
||||
.card-info-footer .star-rating {
|
||||
margin-left: 4px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1rem;
|
||||
width: 0.5rem;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.star-rating.full {
|
||||
color: #E5A00D;
|
||||
@@ -490,6 +422,7 @@
|
||||
.badge {
|
||||
display: inline-block;
|
||||
min-width: 10px;
|
||||
margin-right: 4px;
|
||||
padding: 3px 7px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
@@ -594,37 +527,27 @@
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;font-size: 14px;line-height: 1.4;margin: 0;padding: 0;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
|
||||
<body style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;font-size: 14px;line-height: 1.4;margin: 0;padding: 0;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
|
||||
% if preview and service and service != 'self-hosted':
|
||||
<div class="local-preview-note" style="text-align: center;padding-top: 10px;"><p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;color: #282A2D;font-size: 12px;">Note: Local preview images only - images will be uploaded to ${service.capitalize()} when the newsletter is sent.</p></div> <!-- IGNORE SAVE -->
|
||||
% elif preview and not service:
|
||||
<div class="local-preview-note" style="text-align: center;padding-top: 10px;"><p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;color: #282A2D;font-size: 12px;">Warning: The Image Hosting setting must be enabled for images to display on the newsletter.</p></div> <!-- IGNORE SAVE -->
|
||||
% endif
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
<td class="container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;display: block;max-width: 1042px;padding: 10px;width: 1042px;margin: 0 auto !important;">
|
||||
<div class="content" style="box-sizing: border-box;display: block;margin: 0 auto;max-width: 1037px;padding: 10px;">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<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 -->
|
||||
</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;">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;box-sizing: border-box;padding: 5px;overflow: auto;">
|
||||
<div class="header" style="width: 100%;height: 90px;text-align: center;">
|
||||
@@ -634,7 +557,6 @@
|
||||
<div class="dates" style="color: #aaaaaa;font-size: 20px;text-align: center;">${parameters['start_date']} - ${parameters['end_date']}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
% if message:
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;box-sizing: border-box;padding: 5px;overflow: auto;">
|
||||
@@ -643,7 +565,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
|
||||
% if recently_added.get('movie'):
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;box-sizing: border-box;padding: 5px;overflow: auto;">
|
||||
@@ -659,109 +580,105 @@
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
% for movie in (movie_a, movie_b):
|
||||
% if movie:
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance movie" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 3px;width: 502px;min-width: 502px;max-width: 502px;height: 233px;">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tbody>
|
||||
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% for movie in (movie_a, movie_b):
|
||||
% if movie:
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance movie" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 3px;width: 502px;min-width: 502px;max-width: 502px;height: 233px;">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;">
|
||||
<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 class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;">
|
||||
<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['poster_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);">
|
||||
<tbody>
|
||||
<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;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://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>
|
||||
<td class="card-info-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;padding-left: 4px;text-align: left;height: 227px;">
|
||||
<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%;">
|
||||
<tbody>
|
||||
<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;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.75rem;vertical-align: top;padding: 5px;height: 100%;">
|
||||
% if movie['tagline']:
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
<em>${movie['tagline']}</em>
|
||||
</p>
|
||||
% endif
|
||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.6rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;padding-top: 0px;padding-right: 5px;padding-bottom: 5px;padding-left: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="badge-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;max-width: 265px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;">
|
||||
% if movie['year']:
|
||||
<span class="badge" title="${movie['year']}" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${movie['year']}</span>
|
||||
% endif
|
||||
% if movie['duration']:
|
||||
<% duration = int(int(movie['duration'])/60000) %>
|
||||
<span class="badge" title="${duration} mins" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${duration} mins</span>
|
||||
% endif
|
||||
% if movie['genres']:
|
||||
% for genre in movie['genres'][:2]:
|
||||
<span class="badge" title="${genre}" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${genre}</span>
|
||||
% endfor
|
||||
% endif
|
||||
</td>
|
||||
% if movie['rating']:
|
||||
<% rating = int(round(float(movie['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(movie['rating'])/0.1)}%" align="right" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: bottom;width: 60px;padding-right: 5px;">
|
||||
% for _ in range(rating):
|
||||
<span class="star-rating full" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</span>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<span class="star-rating empty" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</span>
|
||||
% endfor
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="card-info-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;padding-left: 4px;text-align: left;height: 227px;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.75rem;vertical-align: top;padding: 5px;height: 100%;">
|
||||
% if movie['tagline']:
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
<em>${movie['tagline']}</em>
|
||||
</p>
|
||||
% endif
|
||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.6rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;padding-top: 0px;padding-right: 5px;padding-bottom: 5px;padding-left: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
<td class="badge-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;max-width: 260px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% if movie['year']:
|
||||
<td class="badge" title="${movie['year']}" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${movie['year']}</td>
|
||||
% endif
|
||||
% if movie['duration']:
|
||||
<% duration = int(int(movie['duration'])/60000) %>
|
||||
<td class="badge" title="${duration} mins" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${duration} mins</td>
|
||||
% endif
|
||||
% if movie['genres']:
|
||||
% for genre in movie['genres'][:]:
|
||||
<td class="badge" title="${genre}" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${genre}</td>
|
||||
% endfor
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if movie['rating']:
|
||||
<% rating = int(round(float(movie['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(movie['rating'])/0.1)}%" align="right" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 65px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% for _ in range(rating):
|
||||
<td class="star-rating full" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.8rem;vertical-align: bottom;margin-left: 4px;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</td>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<td class="star-rating empty" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.8rem;vertical-align: bottom;margin-left: 4px;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</td>
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -783,142 +700,137 @@
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
% for show_a, show_b in grouper(recently_added['show'], 2):
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
% for show in (show_a, show_b):
|
||||
% if show:
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
<%
|
||||
if show['season_count'] == 1:
|
||||
if show['season'][0]['episode_count'] == 1:
|
||||
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||
else:
|
||||
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
||||
else:
|
||||
link_rating_key = show['rating_key']
|
||||
%>
|
||||
<td align="center" valign="top" class="card-instance show" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 3px;width: 502px;min-width: 502px;max-width: 502px;height: 233px;">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tbody>
|
||||
% for show_a, show_b in grouper(recently_added['show'], 2):
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% for show in (show_a, show_b):
|
||||
% if show:
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
<%
|
||||
if show['season_count'] == 1:
|
||||
if show['season'][0]['episode_count'] == 1:
|
||||
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||
else:
|
||||
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
||||
else:
|
||||
link_rating_key = show['rating_key']
|
||||
%>
|
||||
<td align="center" valign="top" class="card-instance show" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 3px;width: 502px;min-width: 502px;max-width: 502px;height: 233px;">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;">
|
||||
<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 class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;">
|
||||
<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['poster_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);">
|
||||
<tbody>
|
||||
<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;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://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>
|
||||
<td class="card-info-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;padding-left: 4px;text-align: left;height: 227px;">
|
||||
<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%;">
|
||||
<tbody>
|
||||
<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;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.75rem;vertical-align: top;padding: 5px;height: 100%;">
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
% if show['season_count'] > 1:
|
||||
<em>${show['season_count']} seasons /</em>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="card-info-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;padding-left: 4px;text-align: left;height: 227px;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.75rem;vertical-align: top;padding: 5px;height: 100%;">
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
% if show['season_count'] > 1:
|
||||
<em>${show['season_count']} seasons /</em>
|
||||
% endif
|
||||
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
||||
<em>${total_show_episodes} episode${'s' if total_show_episodes > 1 else ''}</em>
|
||||
</p>
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
% for i, season in enumerate(show['season'][:8]):
|
||||
% if season['episode_count'] == 1:
|
||||
Season ${season['media_index']} · Episode ${season['episode'][0]['media_index']} - ${season['episode'][0]['title']}
|
||||
% else:
|
||||
Season ${season['media_index']} · Episodes ${season['episode_range']}
|
||||
% endif
|
||||
% if i < min(show['season_count'], 7):
|
||||
<br>
|
||||
% elif i == 7 and show['season_count'] > 8:
|
||||
...plus ${show['season_count'] - 8} more seasons!
|
||||
% endif
|
||||
% endfor
|
||||
</p>
|
||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||
% if show['season_count'] == 1 and show['season'][0]['episode_count'] == 1:
|
||||
${show['season'][0]['episode'][0]['summary'][:350] + (show['season'][0]['episode'][0]['summary'][350:] and '...')}
|
||||
% else:
|
||||
<% length = max(0, 350 - 50 * (show['season_count'] - 1)) %>
|
||||
% if length:
|
||||
${show['summary'][:length] + (show['summary'][length:] and '...')}
|
||||
% endif
|
||||
% endif
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.6rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;padding-top: 0px;padding-right: 5px;padding-bottom: 5px;padding-left: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
<td class="badge-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;max-width: 260px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% if show['year']:
|
||||
<td class="badge" title="${show['year']}" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${show['year']}</td>
|
||||
% endif
|
||||
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
||||
<em>${total_show_episodes} episode${'s' if total_show_episodes > 1 else ''}</em>
|
||||
</p>
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
% for i, season in enumerate(show['season'][:8]):
|
||||
Season ${season['media_index']} ·
|
||||
% if season['episode_count'] == 1:
|
||||
Episode ${season['episode'][0]['media_index']} - ${season['episode'][0]['title']}
|
||||
% else:
|
||||
Episodes ${season['episode_range']}
|
||||
% endif
|
||||
% if i < min(show['season_count'], 7):
|
||||
<br>
|
||||
% elif i == 7 and show['season_count'] > 8:
|
||||
...plus ${show['season_count'] - 8} more seasons!
|
||||
% endif
|
||||
% if show['duration']:
|
||||
<% duration = int(int(show['duration'])/60000) %>
|
||||
<td class="badge" title="${duration} mins" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${duration} mins</td>
|
||||
% endif
|
||||
% if show['genres']:
|
||||
% for genre in show['genres'][:2]:
|
||||
<td class="badge" title="${genre}" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${genre}</td>
|
||||
% endfor
|
||||
</p>
|
||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||
% if show['season_count'] == 1 and show['season'][0]['episode_count'] == 1:
|
||||
${show['season'][0]['episode'][0]['summary'][:350] + (show['season'][0]['episode'][0]['summary'][350:] and '...')}
|
||||
% else:
|
||||
<% length = max(0, 350 - 50 * (show['season_count'] - 1)) %>
|
||||
% if length:
|
||||
${show['summary'][:length] + (show['summary'][length:] and '...')}
|
||||
% endif
|
||||
% endif
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.6rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;padding-top: 0px;padding-right: 5px;padding-bottom: 5px;padding-left: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="badge-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;max-width: 265px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;">
|
||||
% if show['year']:
|
||||
<span class="badge" title="${show['year']}" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${show['year']}</span>
|
||||
% endif
|
||||
% if show['duration']:
|
||||
<% duration = int(int(show['duration'])/60000) %>
|
||||
<span class="badge" title="${duration} mins" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${duration} mins</span>
|
||||
% endif
|
||||
% if show['genres']:
|
||||
% for genre in show['genres'][:2]:
|
||||
<span class="badge" title="${genre}" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${genre}</span>
|
||||
% endfor
|
||||
% endif
|
||||
</td>
|
||||
% if show['rating']:
|
||||
<% rating = int(round(float(show['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(show['rating'])/0.1)}%" align="right" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: bottom;width: 60px;padding-right: 5px;">
|
||||
% for _ in range(rating):
|
||||
<span class="star-rating full" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</span>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<span class="star-rating empty" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</span>
|
||||
% endfor
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if show['rating']:
|
||||
<% rating = int(round(float(show['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(show['rating'])/0.1)}%" align="right" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 65px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% for _ in range(rating):
|
||||
<td class="star-rating full" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.8rem;vertical-align: bottom;margin-left: 4px;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</td>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<td class="star-rating empty" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.8rem;vertical-align: bottom;margin-left: 4px;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</td>
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -940,124 +852,114 @@
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
% for album_a, album_b in grouper([a for artist in recently_added['artist'] for a in artist['album']], 2):
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
% for album in (album_a, album_b):
|
||||
% if album:
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance album" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 3px;width: 502px;min-width: 502px;max-width: 502px;height: 158px;">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tbody>
|
||||
% for album_a, album_b in grouper([a for artist in recently_added['artist'] for a in artist['album']], 2):
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% for album in (album_a, album_b):
|
||||
% if album:
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance album" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 3px;width: 502px;min-width: 502px;max-width: 502px;height: 158px;">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 152px;">
|
||||
<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 class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 152px;">
|
||||
<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['poster_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);">
|
||||
<tbody>
|
||||
<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;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'http://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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'http://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>
|
||||
<td class="card-info-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;padding-left: 4px;text-align: left;height: 152px;">
|
||||
<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%;">
|
||||
<tbody>
|
||||
<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;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.75rem;vertical-align: top;padding: 5px;height: 82px;min-height: 82px;">
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
||||
</p>
|
||||
% if artist['title'].lower() != 'various artists':
|
||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
||||
</p>
|
||||
% endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.6rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;padding-top: 0px;padding-right: 5px;padding-bottom: 5px;padding-left: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="badge-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;max-width: 265px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;">
|
||||
% if album['year']:
|
||||
<span class="badge" title="${album['year']}" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${album['year']}</span>
|
||||
% endif
|
||||
% if album['genres']:
|
||||
% for genre in album['genres'][:2]:
|
||||
<span class="badge" title="${genre}" style="display: inline-block;min-width: 10px;padding: 3px 7px;font-size: 11px;line-height: 1;text-align: center;white-space: nowrap;vertical-align: middle;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${genre}</span>
|
||||
% endfor
|
||||
% endif
|
||||
</td>
|
||||
% if album['rating']:
|
||||
<% rating = int(round(float(album['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(album['rating'])/0.1)}%" align="right" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: bottom;width: 60px;padding-right: 5px;">
|
||||
% for _ in range(rating):
|
||||
<span class="star-rating full" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</span>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<span class="star-rating empty" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</span>
|
||||
% endfor
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="card-info-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;padding-left: 4px;text-align: left;height: 152px;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.75rem;vertical-align: top;padding: 5px;height: 82px;min-height: 82px;">
|
||||
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
|
||||
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
||||
</p>
|
||||
% if album['parent_title'].lower() != 'various artists':
|
||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
||||
</p>
|
||||
% endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.6rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;padding-top: 0px;padding-right: 5px;padding-bottom: 5px;padding-left: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
<td class="badge-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;max-width: 260px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% if album['year']:
|
||||
<td class="badge" title="${album['year']}" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${album['year']}</td>
|
||||
% endif
|
||||
% if album['genres']:
|
||||
% for genre in album['genres'][:2]:
|
||||
<td class="badge" title="${genre}" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 11px;vertical-align: middle;display: inline-block;min-width: 10px;margin-right: 4px;padding: 3px 7px;line-height: 1;text-align: center;white-space: nowrap;background-color: rgba(0, 0, 0, .25);border-radius: 2px;text-overflow: ellipsis;overflow: hidden;color: #ffffff;">${genre}</td>
|
||||
% endfor
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if album['rating']:
|
||||
<% rating = int(round(float(album['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(album['rating'])/0.1)}%" align="right" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 65px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||
<tr>
|
||||
% for _ in range(rating):
|
||||
<td class="star-rating full" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.8rem;vertical-align: bottom;margin-left: 4px;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</td>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<td class="star-rating empty" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.8rem;vertical-align: bottom;margin-left: 4px;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</td>
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;overflow: hidden;padding: 0 !important;width: 251px !important;min-width: 251px !important;max-width: 251px !important;"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td class="footer" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;clear: both;margin-top: 10px;text-align: center;width: 100%;">
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer-bar" style="margin-left: auto;margin-right: auto;width: 200px;border-top: 1px solid #E5A00D;margin-top: 25px;"></div>
|
||||
<div class="content-block powered-by" style="padding-bottom: 10px;padding-top: 10px;">
|
||||
Newsletter generated by <a href="http://tautulli.com" target="_blank" style="text-decoration: underline;color: #fff;font-size: 12px;text-align: center;">Tautulli</a>.
|
||||
<!-- FOOTER MESSAGE - DO NOT REMOVE -->
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@@ -1,17 +1,24 @@
|
||||
% if data:
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy.helpers import grouper
|
||||
from plexpy.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/'
|
||||
base_url_image = base_url + 'image/'
|
||||
elif preview:
|
||||
base_url = 'newsletter/'
|
||||
base_url_image = base_url + 'image/'
|
||||
else:
|
||||
base_url = base_url_image = ''
|
||||
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/'
|
||||
elif preview and service and service != 'self-hosted':
|
||||
base_url_image = 'image/'
|
||||
else:
|
||||
base_url_image = ''
|
||||
|
||||
%>
|
||||
<!doctype html>
|
||||
<html>
|
||||
@@ -61,17 +68,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 1042px;
|
||||
padding: 10px;
|
||||
width: 1042px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
@@ -83,6 +87,14 @@
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.local-preview-note {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.local-preview-note p {
|
||||
color: #282A2D;
|
||||
font-size: 12px;
|
||||
}
|
||||
.main {
|
||||
background: #282A2D;
|
||||
border-radius: 3px;
|
||||
@@ -157,88 +169,9 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.mb5 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@@ -248,9 +181,6 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
@@ -309,6 +239,7 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
MEDIA SECTIONS
|
||||
------------------------------------- */
|
||||
@@ -363,6 +294,7 @@
|
||||
font-size: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
MEDIA CARDS
|
||||
------------------------------------- */
|
||||
@@ -436,6 +368,7 @@
|
||||
line-height: 1.2rem;
|
||||
font-size: 0.9rem;
|
||||
padding: 5px;
|
||||
max-width: 320px;
|
||||
}
|
||||
.card-info-title a {
|
||||
text-decoration: none;
|
||||
@@ -465,21 +398,21 @@
|
||||
padding-left: 5px;
|
||||
}
|
||||
.card-info-footer .badge-container {
|
||||
max-width: 265px;
|
||||
max-width: 260px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.card-info-footer .star-rating-container {
|
||||
width: 60px;
|
||||
vertical-align: bottom;
|
||||
padding-right: 5px;
|
||||
width: 65px;
|
||||
}
|
||||
.card-info-footer .star-rating {
|
||||
margin-left: 4px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1rem;
|
||||
width: 0.5rem;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.star-rating.full {
|
||||
color: #E5A00D;
|
||||
@@ -490,6 +423,7 @@
|
||||
.badge {
|
||||
display: inline-block;
|
||||
min-width: 10px;
|
||||
margin-right: 4px;
|
||||
padding: 3px 7px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
@@ -594,37 +528,27 @@
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
<body>
|
||||
% if preview and service and service != 'self-hosted':
|
||||
<div class="local-preview-note"><p>Note: Local preview images only - images will be uploaded to ${service.capitalize()} when the newsletter is sent.</p></div> <!-- IGNORE SAVE -->
|
||||
% elif preview and not service:
|
||||
<div class="local-preview-note"><p>Warning: The Image Hosting setting must be enabled for images to display on the newsletter.</p></div> <!-- IGNORE SAVE -->
|
||||
% endif
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<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 -->
|
||||
</div> <!-- IGNORE SAVE -->
|
||||
% endif
|
||||
|
||||
<table border="0" cellpadding="3" cellspacing="0" class="main">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<div class="header">
|
||||
@@ -634,7 +558,6 @@
|
||||
<div class="dates">${parameters['start_date']} - ${parameters['end_date']}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
% if message:
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
@@ -643,7 +566,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
|
||||
% if recently_added.get('movie'):
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
@@ -659,109 +581,105 @@
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
% for movie in (movie_a, movie_b):
|
||||
% if movie:
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance movie">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});">
|
||||
<tbody>
|
||||
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
% for movie in (movie_a, movie_b):
|
||||
% if movie:
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance movie">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});">
|
||||
<tr>
|
||||
<td class="card-poster-container">
|
||||
<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 class="card-poster-container">
|
||||
<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['poster_url']})">
|
||||
<tbody>
|
||||
<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">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
<td class="card-info-container">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tbody>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body">
|
||||
% if movie['tagline']:
|
||||
<p class="nowrap mb5">
|
||||
<em>${movie['tagline']}</em>
|
||||
</p>
|
||||
% endif
|
||||
<p>
|
||||
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="badge-container">
|
||||
% if movie['year']:
|
||||
<span class="badge" title="${movie['year']}">${movie['year']}</span>
|
||||
% endif
|
||||
% if movie['duration']:
|
||||
<% duration = int(int(movie['duration'])/60000) %>
|
||||
<span class="badge" title="${duration} mins">${duration} mins</span>
|
||||
% endif
|
||||
% if movie['genres']:
|
||||
% for genre in movie['genres'][:2]:
|
||||
<span class="badge" title="${genre}">${genre}</span>
|
||||
% endfor
|
||||
% endif
|
||||
</td>
|
||||
% if movie['rating']:
|
||||
<% rating = int(round(float(movie['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(movie['rating'])/0.1)}%" align="right">
|
||||
% for _ in range(rating):
|
||||
<span class="star-rating full">★</span>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<span class="star-rating empty">☆</span>
|
||||
% endfor
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="card-info-container">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body">
|
||||
% if movie['tagline']:
|
||||
<p class="nowrap mb5">
|
||||
<em>${movie['tagline']}</em>
|
||||
</p>
|
||||
% endif
|
||||
<p>
|
||||
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="badge-container">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
% if movie['year']:
|
||||
<td class="badge" title="${movie['year']}">${movie['year']}</td>
|
||||
% endif
|
||||
% if movie['duration']:
|
||||
<% duration = int(int(movie['duration'])/60000) %>
|
||||
<td class="badge" title="${duration} mins">${duration} mins</td>
|
||||
% endif
|
||||
% if movie['genres']:
|
||||
% for genre in movie['genres'][:]:
|
||||
<td class="badge" title="${genre}">${genre}</td>
|
||||
% endfor
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if movie['rating']:
|
||||
<% rating = int(round(float(movie['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(movie['rating'])/0.1)}%" align="right">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
% for _ in range(rating):
|
||||
<td class="star-rating full">★</td>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<td class="star-rating empty">☆</td>
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if not movie_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -783,142 +701,137 @@
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
% for show_a, show_b in grouper(recently_added['show'], 2):
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
% for show in (show_a, show_b):
|
||||
% if show:
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
<%
|
||||
if show['season_count'] == 1:
|
||||
if show['season'][0]['episode_count'] == 1:
|
||||
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||
else:
|
||||
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
||||
else:
|
||||
link_rating_key = show['rating_key']
|
||||
%>
|
||||
<td align="center" valign="top" class="card-instance show">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});">
|
||||
<tbody>
|
||||
% for show_a, show_b in grouper(recently_added['show'], 2):
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
% for show in (show_a, show_b):
|
||||
% if show:
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
<%
|
||||
if show['season_count'] == 1:
|
||||
if show['season'][0]['episode_count'] == 1:
|
||||
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||
else:
|
||||
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
||||
else:
|
||||
link_rating_key = show['rating_key']
|
||||
%>
|
||||
<td align="center" valign="top" class="card-instance show">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});">
|
||||
<tr>
|
||||
<td class="card-poster-container">
|
||||
<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 class="card-poster-container">
|
||||
<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['poster_url']})">
|
||||
<tbody>
|
||||
<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">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'http://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
<td class="card-info-container">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tbody>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body">
|
||||
<p class="nowrap mb5">
|
||||
% if show['season_count'] > 1:
|
||||
<em>${show['season_count']} seasons /</em>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="card-info-container">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body">
|
||||
<p class="nowrap mb5">
|
||||
% if show['season_count'] > 1:
|
||||
<em>${show['season_count']} seasons /</em>
|
||||
% endif
|
||||
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
||||
<em>${total_show_episodes} episode${'s' if total_show_episodes > 1 else ''}</em>
|
||||
</p>
|
||||
<p class="nowrap mb5">
|
||||
% for i, season in enumerate(show['season'][:8]):
|
||||
% if season['episode_count'] == 1:
|
||||
Season ${season['media_index']} · Episode ${season['episode'][0]['media_index']} - ${season['episode'][0]['title']}
|
||||
% else:
|
||||
Season ${season['media_index']} · Episodes ${season['episode_range']}
|
||||
% endif
|
||||
% if i < min(show['season_count'], 7):
|
||||
<br>
|
||||
% elif i == 7 and show['season_count'] > 8:
|
||||
...plus ${show['season_count'] - 8} more seasons!
|
||||
% endif
|
||||
% endfor
|
||||
</p>
|
||||
<p>
|
||||
% if show['season_count'] == 1 and show['season'][0]['episode_count'] == 1:
|
||||
${show['season'][0]['episode'][0]['summary'][:350] + (show['season'][0]['episode'][0]['summary'][350:] and '...')}
|
||||
% else:
|
||||
<% length = max(0, 350 - 50 * (show['season_count'] - 1)) %>
|
||||
% if length:
|
||||
${show['summary'][:length] + (show['summary'][length:] and '...')}
|
||||
% endif
|
||||
% endif
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="badge-container">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
% if show['year']:
|
||||
<td class="badge" title="${show['year']}">${show['year']}</td>
|
||||
% endif
|
||||
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
||||
<em>${total_show_episodes} episode${'s' if total_show_episodes > 1 else ''}</em>
|
||||
</p>
|
||||
<p class="nowrap mb5">
|
||||
% for i, season in enumerate(show['season'][:8]):
|
||||
Season ${season['media_index']} ·
|
||||
% if season['episode_count'] == 1:
|
||||
Episode ${season['episode'][0]['media_index']} - ${season['episode'][0]['title']}
|
||||
% else:
|
||||
Episodes ${season['episode_range']}
|
||||
% endif
|
||||
% if i < min(show['season_count'], 7):
|
||||
<br>
|
||||
% elif i == 7 and show['season_count'] > 8:
|
||||
...plus ${show['season_count'] - 8} more seasons!
|
||||
% endif
|
||||
% if show['duration']:
|
||||
<% duration = int(int(show['duration'])/60000) %>
|
||||
<td class="badge" title="${duration} mins">${duration} mins</td>
|
||||
% endif
|
||||
% if show['genres']:
|
||||
% for genre in show['genres'][:2]:
|
||||
<td class="badge" title="${genre}">${genre}</td>
|
||||
% endfor
|
||||
</p>
|
||||
<p>
|
||||
% if show['season_count'] == 1 and show['season'][0]['episode_count'] == 1:
|
||||
${show['season'][0]['episode'][0]['summary'][:350] + (show['season'][0]['episode'][0]['summary'][350:] and '...')}
|
||||
% else:
|
||||
<% length = max(0, 350 - 50 * (show['season_count'] - 1)) %>
|
||||
% if length:
|
||||
${show['summary'][:length] + (show['summary'][length:] and '...')}
|
||||
% endif
|
||||
% endif
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="badge-container">
|
||||
% if show['year']:
|
||||
<span class="badge" title="${show['year']}">${show['year']}</span>
|
||||
% endif
|
||||
% if show['duration']:
|
||||
<% duration = int(int(show['duration'])/60000) %>
|
||||
<span class="badge" title="${duration} mins">${duration} mins</span>
|
||||
% endif
|
||||
% if show['genres']:
|
||||
% for genre in show['genres'][:2]:
|
||||
<span class="badge" title="${genre}">${genre}</span>
|
||||
% endfor
|
||||
% endif
|
||||
</td>
|
||||
% if show['rating']:
|
||||
<% rating = int(round(float(show['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(show['rating'])/0.1)}%" align="right">
|
||||
% for _ in range(rating):
|
||||
<span class="star-rating full">★</span>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<span class="star-rating empty">☆</span>
|
||||
% endfor
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if show['rating']:
|
||||
<% rating = int(round(float(show['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(show['rating'])/0.1)}%" align="right">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
% for _ in range(rating):
|
||||
<td class="star-rating full">★</td>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<td class="star-rating empty">☆</td>
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if not show_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -940,124 +853,114 @@
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
% for album_a, album_b in grouper([a for artist in recently_added['artist'] for a in artist['album']], 2):
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
% for album in (album_a, album_b):
|
||||
% if album:
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance album">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});">
|
||||
<tbody>
|
||||
% for album_a, album_b in grouper([a for artist in recently_added['artist'] for a in artist['album']], 2):
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
% for album in (album_a, album_b):
|
||||
% if album:
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
<td align="center" valign="top" class="card-instance album">
|
||||
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});">
|
||||
<tr>
|
||||
<td class="card-poster-container">
|
||||
<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 class="card-poster-container">
|
||||
<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['poster_url']})">
|
||||
<tbody>
|
||||
<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">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'http://tautulli.com/images/newsletter/view-on-plex-cover.png'}" width="150" height="150">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'http://tautulli.com/images/newsletter/view-on-plex-cover.png'}" width="150" height="150">
|
||||
</a>
|
||||
</td>
|
||||
<td class="card-info-container">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tbody>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body">
|
||||
<p class="nowrap mb5">
|
||||
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
||||
</p>
|
||||
% if artist['title'].lower() != 'various artists':
|
||||
<p>
|
||||
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
||||
</p>
|
||||
% endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="badge-container">
|
||||
% if album['year']:
|
||||
<span class="badge" title="${album['year']}">${album['year']}</span>
|
||||
% endif
|
||||
% if album['genres']:
|
||||
% for genre in album['genres'][:2]:
|
||||
<span class="badge" title="${genre}">${genre}</span>
|
||||
% endfor
|
||||
% endif
|
||||
</td>
|
||||
% if album['rating']:
|
||||
<% rating = int(round(float(album['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(album['rating'])/0.1)}%" align="right">
|
||||
% for _ in range(rating):
|
||||
<span class="star-rating full">★</span>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<span class="star-rating empty">☆</span>
|
||||
% endfor
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td class="card-info-container">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-body">
|
||||
<p class="nowrap mb5">
|
||||
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
||||
</p>
|
||||
% if album['parent_title'].lower() != 'various artists':
|
||||
<p>
|
||||
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
||||
</p>
|
||||
% endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="card-info-footer nowrap">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="badge-container">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
% if album['year']:
|
||||
<td class="badge" title="${album['year']}">${album['year']}</td>
|
||||
% endif
|
||||
% if album['genres']:
|
||||
% for genre in album['genres'][:2]:
|
||||
<td class="badge" title="${genre}">${genre}</td>
|
||||
% endfor
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if album['rating']:
|
||||
<% rating = int(round(float(album['rating']) / 2)) %>
|
||||
<td class="star-rating-container" title="${int(float(album['rating'])/0.1)}%" align="right">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
% for _ in range(rating):
|
||||
<td class="star-rating full">★</td>
|
||||
% endfor
|
||||
% for _ in range(5-rating):
|
||||
<td class="star-rating empty">☆</td>
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% endif
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
% if not album_b:
|
||||
<td align="center" valign="top" class="card-instance pad"></td>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td class="footer">
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer-bar"></div>
|
||||
<div class="content-block powered-by">
|
||||
Newsletter generated by <a href="http://tautulli.com" target="_blank">Tautulli</a>.
|
||||
<!-- FOOTER MESSAGE - DO NOT REMOVE -->
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
@@ -9,7 +9,7 @@ __all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
|
||||
'WeekdayPositionExpression', 'LastDayOfMonthExpression')
|
||||
|
||||
|
||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
||||
|
||||
|
||||
|
@@ -23,7 +23,7 @@ __author__ = 'The Python-Twitter Developers'
|
||||
__email__ = 'python-twitter@googlegroups.com'
|
||||
__copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers'
|
||||
__license__ = 'Apache License 2.0'
|
||||
__version__ = '3.0rc1'
|
||||
__version__ = '3.4.1'
|
||||
__url__ = 'https://github.com/bear/python-twitter'
|
||||
__download_url__ = 'https://pypi.python.org/pypi/python-twitter'
|
||||
__description__ = 'A Python wrapper around the Twitter API'
|
||||
|
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from hashlib import md5
|
||||
@@ -47,7 +46,7 @@ class _FileCache(object):
|
||||
path = self._GetPath(key)
|
||||
if not path.startswith(self._root_directory):
|
||||
raise _FileCacheError('%s does not appear to live under %s' %
|
||||
(path, self._root_directory ))
|
||||
(path, self._root_directory))
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
@@ -101,61 +100,3 @@ class _FileCache(object):
|
||||
|
||||
def _GetPrefix(self, hashed_key):
|
||||
return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])
|
||||
|
||||
|
||||
class ParseTweet(object):
|
||||
# compile once on import
|
||||
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
|
||||
"HASHTAG": r"(#[\w\d]+)", "URL": r"([http://]?[a-zA-Z\d\/]+[\.]+[a-zA-Z\d\/\.]+)"}
|
||||
regexp = dict((key, re.compile(value)) for key, value in list(regexp.items()))
|
||||
|
||||
def __init__(self, timeline_owner, tweet):
|
||||
""" timeline_owner : twitter handle of user account. tweet - 140 chars from feed; object does all computation on construction
|
||||
properties:
|
||||
RT, MT - boolean
|
||||
URLs - list of URL
|
||||
Hashtags - list of tags
|
||||
"""
|
||||
self.Owner = timeline_owner
|
||||
self.tweet = tweet
|
||||
self.UserHandles = ParseTweet.getUserHandles(tweet)
|
||||
self.Hashtags = ParseTweet.getHashtags(tweet)
|
||||
self.URLs = ParseTweet.getURLs(tweet)
|
||||
self.RT = ParseTweet.getAttributeRT(tweet)
|
||||
self.MT = ParseTweet.getAttributeMT(tweet)
|
||||
|
||||
# additional intelligence
|
||||
if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet?
|
||||
self.Owner = self.UserHandles[0]
|
||||
return
|
||||
|
||||
def __str__(self):
|
||||
""" for display method """
|
||||
return "owner %s, urls: %d, hashtags %d, user_handles %d, len_tweet %d, RT = %s, MT = %s" % (
|
||||
self.Owner, len(self.URLs), len(self.Hashtags), len(self.UserHandles),
|
||||
len(self.tweet), self.RT, self.MT)
|
||||
|
||||
@staticmethod
|
||||
def getAttributeRT(tweet):
|
||||
""" see if tweet is a RT """
|
||||
return re.search(ParseTweet.regexp["RT"], tweet.strip()) is not None
|
||||
|
||||
@staticmethod
|
||||
def getAttributeMT(tweet):
|
||||
""" see if tweet is a MT """
|
||||
return re.search(ParseTweet.regexp["MT"], tweet.strip()) is not None
|
||||
|
||||
@staticmethod
|
||||
def getUserHandles(tweet):
|
||||
""" given a tweet we try and extract all user handles in order of occurrence"""
|
||||
return re.findall(ParseTweet.regexp["ALNUM"], tweet)
|
||||
|
||||
@staticmethod
|
||||
def getHashtags(tweet):
|
||||
""" return all hashtags"""
|
||||
return re.findall(ParseTweet.regexp["HASHTAG"], tweet)
|
||||
|
||||
@staticmethod
|
||||
def getURLs(tweet):
|
||||
""" URL : [http://]?[\w\.?/]+"""
|
||||
return re.findall(ParseTweet.regexp["URL"], tweet)
|
||||
|
2183
lib/twitter/api.py
2183
lib/twitter/api.py
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,18 @@ class TwitterError(Exception):
|
||||
def message(self):
|
||||
'''Returns the first argument used to construct this error.'''
|
||||
return self.args[0]
|
||||
|
||||
|
||||
class PythonTwitterDeprecationWarning(DeprecationWarning):
|
||||
"""Base class for python-twitter deprecation warnings"""
|
||||
pass
|
||||
|
||||
|
||||
class PythonTwitterDeprecationWarning330(PythonTwitterDeprecationWarning):
|
||||
"""Warning for features to be removed in version 3.3.0"""
|
||||
pass
|
||||
|
||||
|
||||
class PythonTwitterDeprecationWarning340(PythonTwitterDeprecationWarning):
|
||||
"""Warning for features to be removed in version 3.4.0"""
|
||||
pass
|
||||
|
@@ -28,6 +28,13 @@ class TwitterModel(object):
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
if hasattr(self, 'id'):
|
||||
return hash(self.id)
|
||||
else:
|
||||
raise TypeError('unhashable type: {} (no id attribute)'
|
||||
.format(type(self)))
|
||||
|
||||
def AsJsonString(self):
|
||||
""" Returns the TwitterModel as a JSON string based on key/value
|
||||
pairs returned from the AsDict() method. """
|
||||
@@ -78,11 +85,14 @@ class TwitterModel(object):
|
||||
|
||||
"""
|
||||
|
||||
json_data = data.copy()
|
||||
if kwargs:
|
||||
for key, val in kwargs.items():
|
||||
data[key] = val
|
||||
json_data[key] = val
|
||||
|
||||
return cls(**data)
|
||||
c = cls(**json_data)
|
||||
c._json = data
|
||||
return c
|
||||
|
||||
|
||||
class Media(TwitterModel):
|
||||
@@ -93,11 +103,14 @@ class Media(TwitterModel):
|
||||
self.param_defaults = {
|
||||
'display_url': None,
|
||||
'expanded_url': None,
|
||||
'ext_alt_text': None,
|
||||
'id': None,
|
||||
'media_url': None,
|
||||
'media_url_https': None,
|
||||
'sizes': None,
|
||||
'type': None,
|
||||
'url': None,
|
||||
'video_info': None,
|
||||
}
|
||||
|
||||
for (param, default) in self.param_defaults.items():
|
||||
@@ -172,8 +185,10 @@ class DirectMessage(TwitterModel):
|
||||
self.param_defaults = {
|
||||
'created_at': None,
|
||||
'id': None,
|
||||
'recipient': None,
|
||||
'recipient_id': None,
|
||||
'recipient_screen_name': None,
|
||||
'sender': None,
|
||||
'sender_id': None,
|
||||
'sender_screen_name': None,
|
||||
'text': None,
|
||||
@@ -181,6 +196,10 @@ class DirectMessage(TwitterModel):
|
||||
|
||||
for (param, default) in self.param_defaults.items():
|
||||
setattr(self, param, kwargs.get(param, default))
|
||||
if 'sender' in kwargs:
|
||||
self.sender = User.NewFromJsonDict(kwargs.get('sender', None))
|
||||
if 'recipient' in kwargs:
|
||||
self.recipient = User.NewFromJsonDict(kwargs.get('recipient', None))
|
||||
|
||||
def __repr__(self):
|
||||
if self.text and len(self.text) > 140:
|
||||
@@ -206,7 +225,7 @@ class Trend(TwitterModel):
|
||||
'query': None,
|
||||
'timestamp': None,
|
||||
'url': None,
|
||||
'volume': None,
|
||||
'tweet_volume': None,
|
||||
}
|
||||
|
||||
for (param, default) in self.param_defaults.items():
|
||||
@@ -218,6 +237,10 @@ class Trend(TwitterModel):
|
||||
self.timestamp,
|
||||
self.url)
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
return self.tweet_volume
|
||||
|
||||
|
||||
class Hashtag(TwitterModel):
|
||||
|
||||
@@ -259,12 +282,12 @@ class UserStatus(TwitterModel):
|
||||
""" A class representing the UserStatus structure. This is an abbreviated
|
||||
form of the twitter.User object. """
|
||||
|
||||
connections = {'following': False,
|
||||
'followed_by': False,
|
||||
'following_received': False,
|
||||
'following_requested': False,
|
||||
'blocking': False,
|
||||
'muting': False}
|
||||
_connections = {'following': False,
|
||||
'followed_by': False,
|
||||
'following_received': False,
|
||||
'following_requested': False,
|
||||
'blocking': False,
|
||||
'muting': False}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.param_defaults = {
|
||||
@@ -284,10 +307,19 @@ class UserStatus(TwitterModel):
|
||||
setattr(self, param, kwargs.get(param, default))
|
||||
|
||||
if 'connections' in kwargs:
|
||||
for param in self.connections:
|
||||
for param in self._connections:
|
||||
if param in kwargs['connections']:
|
||||
setattr(self, param, True)
|
||||
|
||||
@property
|
||||
def connections(self):
|
||||
return {'following': self.following,
|
||||
'followed_by': self.followed_by,
|
||||
'following_received': self.following_received,
|
||||
'following_requested': self.following_requested,
|
||||
'blocking': self.blocking,
|
||||
'muting': self.muting}
|
||||
|
||||
def __repr__(self):
|
||||
connections = [param for param in self.connections if getattr(self, param)]
|
||||
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
|
||||
@@ -307,11 +339,14 @@ class User(TwitterModel):
|
||||
'default_profile': None,
|
||||
'default_profile_image': None,
|
||||
'description': None,
|
||||
'email': None,
|
||||
'favourites_count': None,
|
||||
'followers_count': None,
|
||||
'following': None,
|
||||
'friends_count': None,
|
||||
'geo_enabled': None,
|
||||
'id': None,
|
||||
'id_str': None,
|
||||
'lang': None,
|
||||
'listed_count': None,
|
||||
'location': None,
|
||||
@@ -319,12 +354,16 @@ class User(TwitterModel):
|
||||
'notifications': None,
|
||||
'profile_background_color': None,
|
||||
'profile_background_image_url': None,
|
||||
'profile_background_image_url_https': None,
|
||||
'profile_background_tile': None,
|
||||
'profile_banner_url': None,
|
||||
'profile_image_url': None,
|
||||
'profile_image_url_https': None,
|
||||
'profile_link_color': None,
|
||||
'profile_sidebar_border_color': None,
|
||||
'profile_sidebar_fill_color': None,
|
||||
'profile_text_color': None,
|
||||
'profile_use_background_image': None,
|
||||
'protected': None,
|
||||
'screen_name': None,
|
||||
'status': None,
|
||||
@@ -333,6 +372,8 @@ class User(TwitterModel):
|
||||
'url': None,
|
||||
'utc_offset': None,
|
||||
'verified': None,
|
||||
'withheld_in_countries': None,
|
||||
'withheld_scope': None,
|
||||
}
|
||||
|
||||
for (param, default) in self.param_defaults.items():
|
||||
@@ -365,6 +406,7 @@ class Status(TwitterModel):
|
||||
'current_user_retweet': None,
|
||||
'favorite_count': None,
|
||||
'favorited': None,
|
||||
'full_text': None,
|
||||
'geo': None,
|
||||
'hashtags': None,
|
||||
'id': None,
|
||||
@@ -377,6 +419,9 @@ class Status(TwitterModel):
|
||||
'media': None,
|
||||
'place': None,
|
||||
'possibly_sensitive': None,
|
||||
'quoted_status': None,
|
||||
'quoted_status_id': None,
|
||||
'quoted_status_id_str': None,
|
||||
'retweet_count': None,
|
||||
'retweeted': None,
|
||||
'retweeted_status': None,
|
||||
@@ -395,6 +440,11 @@ class Status(TwitterModel):
|
||||
for (param, default) in self.param_defaults.items():
|
||||
setattr(self, param, kwargs.get(param, default))
|
||||
|
||||
if kwargs.get('full_text', None):
|
||||
self.tweet_mode = 'extended'
|
||||
else:
|
||||
self.tweet_mode = 'compatibility'
|
||||
|
||||
@property
|
||||
def created_at_in_seconds(self):
|
||||
""" Get the time this status message was posted, in seconds since
|
||||
@@ -414,17 +464,21 @@ class Status(TwitterModel):
|
||||
string: A string representation of this twitter.Status instance with
|
||||
the ID of status, username and datetime.
|
||||
"""
|
||||
if self.tweet_mode == 'extended':
|
||||
text = self.full_text
|
||||
else:
|
||||
text = self.text
|
||||
if self.user:
|
||||
return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format(
|
||||
self.id,
|
||||
self.user.screen_name,
|
||||
self.created_at,
|
||||
self.text)
|
||||
text)
|
||||
else:
|
||||
return u"Status(ID={0}, Created={1}, Text={2!r})".format(
|
||||
self.id,
|
||||
self.created_at,
|
||||
self.text)
|
||||
text)
|
||||
|
||||
@classmethod
|
||||
def NewFromJsonDict(cls, data, **kwargs):
|
||||
@@ -439,17 +493,25 @@ class Status(TwitterModel):
|
||||
current_user_retweet = None
|
||||
hashtags = None
|
||||
media = None
|
||||
quoted_status = None
|
||||
retweeted_status = None
|
||||
urls = None
|
||||
user = None
|
||||
user_mentions = None
|
||||
|
||||
# for loading extended tweets from the streaming API.
|
||||
if 'extended_tweet' in data:
|
||||
for k, v in data['extended_tweet'].items():
|
||||
data[k] = v
|
||||
|
||||
if 'user' in data:
|
||||
user = User.NewFromJsonDict(data['user'])
|
||||
if 'retweeted_status' in data:
|
||||
retweeted_status = Status.NewFromJsonDict(data['retweeted_status'])
|
||||
if 'current_user_retweet' in data:
|
||||
current_user_retweet = data['current_user_retweet']['id']
|
||||
if 'quoted_status' in data:
|
||||
quoted_status = Status.NewFromJsonDict(data.get('quoted_status'))
|
||||
|
||||
if 'entities' in data:
|
||||
if 'urls' in data['entities']:
|
||||
@@ -470,6 +532,7 @@ class Status(TwitterModel):
|
||||
current_user_retweet=current_user_retweet,
|
||||
hashtags=hashtags,
|
||||
media=media,
|
||||
quoted_status=quoted_status,
|
||||
retweeted_status=retweeted_status,
|
||||
urls=urls,
|
||||
user=user,
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class Emoticons:
|
||||
POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *",
|
||||
":P", ":D", ":d", ":p",
|
||||
@@ -27,6 +28,7 @@ class Emoticons:
|
||||
"[:", ";]"
|
||||
]
|
||||
|
||||
|
||||
class ParseTweet(object):
|
||||
# compile once on import
|
||||
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
|
||||
@@ -51,7 +53,7 @@ class ParseTweet(object):
|
||||
self.Emoticon = ParseTweet.getAttributeEmoticon(tweet)
|
||||
|
||||
# additional intelligence
|
||||
if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet?
|
||||
if (self.RT and len(self.UserHandles) > 0): # change the owner of tweet?
|
||||
self.Owner = self.UserHandles[0]
|
||||
return
|
||||
|
||||
@@ -66,10 +68,10 @@ class ParseTweet(object):
|
||||
emoji = list()
|
||||
for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()):
|
||||
if tok in Emoticons.POSITIVE:
|
||||
emoji.append( tok )
|
||||
emoji.append(tok)
|
||||
continue
|
||||
if tok in Emoticons.NEGATIVE:
|
||||
emoji.append( tok )
|
||||
emoji.append(tok)
|
||||
return emoji
|
||||
|
||||
@staticmethod
|
||||
|
@@ -97,6 +97,7 @@ class RateLimit(object):
|
||||
and a dictionary of limit, remaining, and reset will be returned.
|
||||
|
||||
"""
|
||||
self.__dict__['resources'] = {}
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
@staticmethod
|
||||
@@ -117,10 +118,12 @@ class RateLimit(object):
|
||||
for non_std_endpoint in NON_STANDARD_ENDPOINTS:
|
||||
if re.match(non_std_endpoint.regex, resource):
|
||||
return non_std_endpoint.resource
|
||||
else:
|
||||
return resource
|
||||
return resource
|
||||
|
||||
def set_unknown_limit(self, url, limit, remaining, reset):
|
||||
return self.set_limit(url, limit, remaining, reset)
|
||||
|
||||
def set_limit(self, url, limit, remaining, reset):
|
||||
""" If a resource family is unknown, add it to the object's
|
||||
dictionary. This is to deal with new endpoints being added to
|
||||
the API, but not necessarily to the information returned by
|
||||
@@ -146,13 +149,18 @@ class RateLimit(object):
|
||||
"""
|
||||
endpoint = self.url_to_resource(url)
|
||||
resource_family = endpoint.split('/')[1]
|
||||
self.__dict__['resources'].update(
|
||||
{resource_family: {
|
||||
endpoint: {
|
||||
"limit": limit,
|
||||
"remaining": remaining,
|
||||
"reset": reset
|
||||
}}})
|
||||
new_endpoint = {endpoint: {
|
||||
"limit": enf_type('limit', int, limit),
|
||||
"remaining": enf_type('remaining', int, remaining),
|
||||
"reset": enf_type('reset', int, reset)
|
||||
}}
|
||||
|
||||
if not self.resources.get(resource_family, None):
|
||||
self.resources[resource_family] = {}
|
||||
|
||||
self.__dict__['resources'][resource_family].update(new_endpoint)
|
||||
|
||||
return self.get_limit(url)
|
||||
|
||||
def get_limit(self, url):
|
||||
""" Gets a EndpointRateLimit object for the given url.
|
||||
@@ -181,35 +189,3 @@ class RateLimit(object):
|
||||
return EndpointRateLimit(family_rates['limit'],
|
||||
family_rates['remaining'],
|
||||
family_rates['reset'])
|
||||
|
||||
def set_limit(self, url, limit, remaining, reset):
|
||||
""" Set an endpoint's rate limits. The data used for each of the
|
||||
args should come from Twitter's ``x-rate-limit`` headers.
|
||||
|
||||
Args:
|
||||
url (str):
|
||||
URL of the endpoint being fetched.
|
||||
limit (int):
|
||||
Max number of times a user or app can hit the endpoint
|
||||
before being rate limited.
|
||||
remaining (int):
|
||||
Number of times a user or app can access the endpoint
|
||||
before being rate limited.
|
||||
reset (int):
|
||||
Epoch time at which the rate limit window will reset.
|
||||
"""
|
||||
endpoint = self.url_to_resource(url)
|
||||
resource_family = endpoint.split('/')[1]
|
||||
|
||||
try:
|
||||
family_rates = self.resources.get(resource_family).get(endpoint)
|
||||
except AttributeError:
|
||||
self.set_unknown_limit(url, limit, remaining, reset)
|
||||
family_rates = self.resources.get(resource_family).get(endpoint)
|
||||
family_rates['limit'] = enf_type('limit', int, limit)
|
||||
family_rates['remaining'] = enf_type('remaining', int, remaining)
|
||||
family_rates['reset'] = enf_type('reset', int, reset)
|
||||
|
||||
return EndpointRateLimit(family_rates['limit'],
|
||||
family_rates['remaining'],
|
||||
family_rates['reset'])
|
||||
|
@@ -1,13 +1,33 @@
|
||||
# encoding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from tempfile import NamedTemporaryFile
|
||||
from unicodedata import normalize
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
except ImportError:
|
||||
from urlparse import urlparse
|
||||
|
||||
import requests
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from twitter import TwitterError
|
||||
import twitter
|
||||
|
||||
if sys.version_info < (3,):
|
||||
range = xrange
|
||||
|
||||
if sys.version_info > (3,):
|
||||
unicode = str
|
||||
|
||||
CHAR_RANGES = [
|
||||
range(0, 4351),
|
||||
range(8192, 8205),
|
||||
range(8208, 8223),
|
||||
range(8242, 8247)]
|
||||
|
||||
TLDS = [
|
||||
"ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar",
|
||||
@@ -138,7 +158,14 @@ TLDS = [
|
||||
"淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团",
|
||||
"飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"]
|
||||
|
||||
URL_REGEXP = re.compile(r'(?i)((?:https?://|www\\.)*(?:[\w+-_]+[.])(?:' + r'\b|'.join(TLDS) + r'\b|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))+(?:[:\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*)', re.UNICODE)
|
||||
URL_REGEXP = re.compile((
|
||||
r'('
|
||||
r'^(?!(https?://|www\.)?\.|ftps?://|([0-9]+\.){{1,3}}\d+)' # exclude urls that start with "."
|
||||
r'(?:https?://|www\.)*^(?!.*@)(?:[\w+-_]+[.])' # beginning of url
|
||||
r'(?:{0}\b' # all tlds
|
||||
r'(?:[:0-9]))' # port numbers & close off TLDs
|
||||
r'(?:[\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*' # path/query params
|
||||
r')').format(r'\b|'.join(TLDS)), re.U | re.I | re.X)
|
||||
|
||||
|
||||
def calc_expected_status_length(status, short_url_length=23):
|
||||
@@ -153,12 +180,19 @@ def calc_expected_status_length(status, short_url_length=23):
|
||||
Expected length of the status message as an integer.
|
||||
|
||||
"""
|
||||
replaced_chars = 0
|
||||
status_length = len(status)
|
||||
match = re.findall(URL_REGEXP, status)
|
||||
if len(match) >= 1:
|
||||
replaced_chars = len(''.join(match))
|
||||
status_length = status_length - replaced_chars + (short_url_length * len(match))
|
||||
status_length = 0
|
||||
if isinstance(status, bytes):
|
||||
status = unicode(status)
|
||||
for word in re.split(r'\s', status):
|
||||
if is_url(word):
|
||||
status_length += short_url_length
|
||||
else:
|
||||
for character in word:
|
||||
if any([ord(normalize("NFC", character)) in char_range for char_range in CHAR_RANGES]):
|
||||
status_length += 1
|
||||
else:
|
||||
status_length += 2
|
||||
status_length += len(re.findall(r'\s', status))
|
||||
return status_length
|
||||
|
||||
|
||||
@@ -171,16 +205,14 @@ def is_url(text):
|
||||
Returns:
|
||||
Boolean of whether the text should be treated as a URL or not.
|
||||
"""
|
||||
if re.findall(URL_REGEXP, text):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(re.findall(URL_REGEXP, text))
|
||||
|
||||
|
||||
def http_to_file(http):
|
||||
data_file = NamedTemporaryFile()
|
||||
req = requests.get(http, stream=True)
|
||||
data_file.write(req.raw.data)
|
||||
for chunk in req.iter_content(chunk_size=1024 * 1024):
|
||||
data_file.write(chunk)
|
||||
return data_file
|
||||
|
||||
|
||||
@@ -200,7 +232,8 @@ def parse_media_file(passed_media):
|
||||
'image/gif',
|
||||
'image/bmp',
|
||||
'image/webp']
|
||||
video_formats = ['video/mp4']
|
||||
video_formats = ['video/mp4',
|
||||
'video/quicktime']
|
||||
|
||||
# If passed_media is a string, check if it points to a URL, otherwise,
|
||||
# it should point to local file. Create a reference to a file obj for
|
||||
@@ -208,7 +241,7 @@ def parse_media_file(passed_media):
|
||||
if not hasattr(passed_media, 'read'):
|
||||
if passed_media.startswith('http'):
|
||||
data_file = http_to_file(passed_media)
|
||||
filename = os.path.basename(passed_media)
|
||||
filename = os.path.basename(urlparse(passed_media).path)
|
||||
else:
|
||||
data_file = open(os.path.realpath(passed_media), 'rb')
|
||||
filename = os.path.basename(passed_media)
|
||||
@@ -216,8 +249,8 @@ def parse_media_file(passed_media):
|
||||
# Otherwise, if a file object was passed in the first place,
|
||||
# create the standard reference to media_file (i.e., rename it to fp).
|
||||
else:
|
||||
if passed_media.mode != 'rb':
|
||||
raise TwitterError({'message': 'File mode must be "rb".'})
|
||||
if passed_media.mode not in ['rb', 'rb+', 'w+b']:
|
||||
raise TwitterError('File mode must be "rb" or "rb+"')
|
||||
filename = os.path.basename(passed_media.name)
|
||||
data_file = passed_media
|
||||
|
||||
@@ -226,16 +259,17 @@ def parse_media_file(passed_media):
|
||||
|
||||
try:
|
||||
data_file.seek(0)
|
||||
except:
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
media_type = mimetypes.guess_type(os.path.basename(filename))[0]
|
||||
if media_type in img_formats and file_size > 5 * 1048576:
|
||||
raise TwitterError({'message': 'Images must be less than 5MB.'})
|
||||
elif media_type in video_formats and file_size > 15 * 1048576:
|
||||
raise TwitterError({'message': 'Videos must be less than 15MB.'})
|
||||
elif media_type not in img_formats and media_type not in video_formats:
|
||||
raise TwitterError({'message': 'Media type could not be determined.'})
|
||||
if media_type is not None:
|
||||
if media_type in img_formats and file_size > 5 * 1048576:
|
||||
raise TwitterError({'message': 'Images must be less than 5MB.'})
|
||||
elif media_type in video_formats and file_size > 15 * 1048576:
|
||||
raise TwitterError({'message': 'Videos must be less than 15MB.'})
|
||||
elif media_type not in img_formats and media_type not in video_formats:
|
||||
raise TwitterError({'message': 'Media type could not be determined.'})
|
||||
|
||||
return data_file, filename, file_size, media_type
|
||||
|
||||
@@ -263,3 +297,18 @@ def enf_type(field, _type, val):
|
||||
raise TwitterError({
|
||||
'message': '"{0}" must be type {1}'.format(field, _type.__name__)
|
||||
})
|
||||
|
||||
|
||||
def parse_arg_list(args, attr):
|
||||
out = []
|
||||
if isinstance(args, (str, unicode)):
|
||||
out.append(args)
|
||||
elif isinstance(args, twitter.User):
|
||||
out.append(getattr(args, attr))
|
||||
elif isinstance(args, (list, tuple)):
|
||||
for item in args:
|
||||
if isinstance(item, (str, unicode)):
|
||||
out.append(item)
|
||||
elif isinstance(item, twitter.User):
|
||||
out.append(getattr(item, attr))
|
||||
return ",".join([str(item) for item in out])
|
||||
|
@@ -152,6 +152,17 @@ def initialize(config_file):
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
verbose=VERBOSE)
|
||||
|
||||
logger.info(u"Starting Tautulli {}".format(
|
||||
common.RELEASE
|
||||
))
|
||||
logger.info(u"{} {} ({}{})".format(
|
||||
common.PLATFORM, common.PLATFORM_RELEASE, common.PLATFORM_VERSION,
|
||||
' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else ''
|
||||
))
|
||||
logger.info(u"Python {}".format(
|
||||
sys.version
|
||||
))
|
||||
|
||||
if not CONFIG.BACKUP_DIR:
|
||||
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')
|
||||
if not os.path.exists(CONFIG.BACKUP_DIR):
|
||||
@@ -509,7 +520,8 @@ def dbcheck():
|
||||
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
|
||||
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
|
||||
'ip_address TEXT, machine_id TEXT, player TEXT, product TEXT, platform TEXT, title TEXT, parent_title TEXT, '
|
||||
'grandparent_title TEXT, full_title TEXT, media_index INTEGER, parent_media_index INTEGER, '
|
||||
'grandparent_title TEXT, original_title TEXT, full_title TEXT, '
|
||||
'media_index INTEGER, parent_media_index INTEGER, '
|
||||
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, year INTEGER, '
|
||||
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
|
||||
@@ -529,6 +541,7 @@ def dbcheck():
|
||||
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
|
||||
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
|
||||
'synced_version INTEGER, synced_version_profile TEXT, '
|
||||
'live INTEGER, live_uuid TEXT, '
|
||||
'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, watched INTEGER DEFAULT 0, '
|
||||
'write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)'
|
||||
)
|
||||
@@ -569,8 +582,9 @@ def dbcheck():
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS session_history_metadata (id INTEGER PRIMARY KEY, '
|
||||
'rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||
'title TEXT, parent_title TEXT, grandparent_title TEXT, full_title TEXT, media_index INTEGER, '
|
||||
'parent_media_index INTEGER, section_id INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, '
|
||||
'title TEXT, parent_title TEXT, grandparent_title TEXT, original_title TEXT, full_title TEXT, '
|
||||
'media_index INTEGER, parent_media_index INTEGER, section_id INTEGER, '
|
||||
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, '
|
||||
'art TEXT, media_type TEXT, year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, '
|
||||
'last_viewed_at INTEGER, content_rating TEXT, summary TEXT, tagline TEXT, rating TEXT, '
|
||||
'duration INTEGER DEFAULT 0, guid TEXT, directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT, '
|
||||
@@ -637,7 +651,7 @@ def dbcheck():
|
||||
# newsletters table :: This table keeps record of the newsletter settings
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS newsletters (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'agent_id INTEGER, agent_name TEXT, agent_label TEXT, '
|
||||
'agent_id INTEGER, agent_name TEXT, agent_label TEXT, id_name TEXT NOT NULL DEFAULT "", '
|
||||
'friendly_name TEXT, newsletter_config TEXT, email_config TEXT, '
|
||||
'subject TEXT, body TEXT, message TEXT, '
|
||||
'cron TEXT NOT NULL DEFAULT "0 0 * * 0", active INTEGER DEFAULT 0)'
|
||||
@@ -648,7 +662,7 @@ def dbcheck():
|
||||
'CREATE TABLE IF NOT EXISTS newsletter_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, '
|
||||
'newsletter_id INTEGER, agent_id INTEGER, agent_name TEXT, notify_action TEXT, '
|
||||
'subject_text TEXT, body_text TEXT, message_text TEXT, start_date TEXT, end_date TEXT, '
|
||||
'start_time INTEGER, end_time INTEGER, uuid TEXT UNIQUE, success INTEGER DEFAULT 0)'
|
||||
'start_time INTEGER, end_time INTEGER, uuid TEXT UNIQUE, filename TEXT, success INTEGER DEFAULT 0)'
|
||||
)
|
||||
|
||||
# recently_added table :: This table keeps record of recently added items
|
||||
@@ -1053,6 +1067,27 @@ def dbcheck():
|
||||
'ALTER TABLE sessions ADD COLUMN watched INTEGER DEFAULT 0'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT live FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN live INTEGER'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN live_uuid TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT original_title FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN original_title TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT reference_id FROM session_history')
|
||||
@@ -1139,6 +1174,15 @@ def dbcheck():
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN labels TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history_metadata table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT original_title FROM session_history_metadata')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table session_history_metadata.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN original_title TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT transcode_decision FROM session_history_media_info')
|
||||
@@ -1495,6 +1539,24 @@ def dbcheck():
|
||||
'ALTER TABLE newsletter_log ADD COLUMN end_time INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade newsletter_log table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT filename FROM newsletter_log')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table newsletter_log.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE newsletter_log ADD COLUMN filename TEXT'
|
||||
)
|
||||
|
||||
# Upgrade newsletters table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT id_name FROM newsletters')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table newsletters.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE newsletters ADD COLUMN id_name TEXT NOT NULL DEFAULT ""'
|
||||
)
|
||||
|
||||
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
|
||||
try:
|
||||
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()
|
||||
@@ -1694,9 +1756,9 @@ def dbcheck():
|
||||
for row in result:
|
||||
img_hash = notification_handler.set_hash_image_info(
|
||||
rating_key=row['rating_key'], width=1000, height=1500, fallback='poster')
|
||||
data_factory.set_imgur_info(img_hash=img_hash, imgur_title=row['poster_title'],
|
||||
imgur_url=row['poster_url'], delete_hash=row['delete_hash'],
|
||||
service='imgur')
|
||||
data_factory.set_img_info(img_hash=img_hash, img_title=row['poster_title'],
|
||||
img_url=row['poster_url'], delete_hash=row['delete_hash'],
|
||||
service='imgur')
|
||||
|
||||
db.action('DROP TABLE poster_urls')
|
||||
except sqlite3.OperationalError:
|
||||
@@ -1794,7 +1856,8 @@ def initialize_tracker():
|
||||
'appVersion': common.RELEASE,
|
||||
'appId': plexpy.INSTALL_TYPE,
|
||||
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
|
||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_VERSION), # App Platform
|
||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
||||
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
||||
'userLanguage': plexpy.SYS_LANGUAGE,
|
||||
'documentEncoding': plexpy.SYS_ENCODING,
|
||||
'noninteractive': True
|
||||
|
@@ -226,7 +226,11 @@ class ActivityHandler(object):
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
this_state = self.timeline['state']
|
||||
this_key = str(self.timeline['ratingKey'])
|
||||
this_rating_key = str(self.timeline['ratingKey'])
|
||||
this_key = self.timeline['key']
|
||||
|
||||
# Get the live tv session uuid
|
||||
this_live_uuid = this_key.split('/')[-1] if this_key.startswith('/livetv/sessions') else None
|
||||
|
||||
# If we already have this session in the temp table, check for state changes
|
||||
if db_session:
|
||||
@@ -235,10 +239,11 @@ class ActivityHandler(object):
|
||||
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
||||
|
||||
last_state = db_session['state']
|
||||
last_key = str(db_session['rating_key'])
|
||||
last_rating_key = str(db_session['rating_key'])
|
||||
last_live_uuid = db_session['live_uuid']
|
||||
|
||||
# Make sure the same item is being played
|
||||
if this_key == last_key:
|
||||
if this_rating_key == last_rating_key or this_live_uuid == last_live_uuid:
|
||||
# Update the session state and viewOffset
|
||||
if this_state == 'playing':
|
||||
# Update the session in our temp session table
|
||||
|
@@ -50,6 +50,7 @@ class ActivityProcessor(object):
|
||||
'title': session.get('title', ''),
|
||||
'parent_title': session.get('parent_title', ''),
|
||||
'grandparent_title': session.get('grandparent_title', ''),
|
||||
'original_title': session.get('original_title', ''),
|
||||
'full_title': session.get('full_title', ''),
|
||||
'media_index': session.get('media_index', ''),
|
||||
'parent_media_index': session.get('parent_media_index', ''),
|
||||
@@ -60,6 +61,7 @@ class ActivityProcessor(object):
|
||||
'friendly_name': session.get('friendly_name', ''),
|
||||
'ip_address': session.get('ip_address', ''),
|
||||
'player': session.get('player', ''),
|
||||
'product': session.get('product', ''),
|
||||
'platform': session.get('platform', ''),
|
||||
'parent_rating_key': session.get('parent_rating_key', ''),
|
||||
'grandparent_rating_key': session.get('grandparent_rating_key', ''),
|
||||
@@ -114,7 +116,9 @@ class ActivityProcessor(object):
|
||||
'stream_audio_channels': session.get('stream_audio_channels', ''),
|
||||
'stream_subtitle_decision': session.get('stream_subtitle_decision', ''),
|
||||
'stream_subtitle_codec': session.get('stream_subtitle_codec', ''),
|
||||
'subtitles': session.get('subtitles', ''),
|
||||
'subtitles': session.get('subtitles', 0),
|
||||
'live': session.get('live', 0),
|
||||
'live_uuid': session.get('live_uuid', ''),
|
||||
'raw_stream_info': json.dumps(session),
|
||||
'stopped': int(time.time())
|
||||
}
|
||||
@@ -180,8 +184,9 @@ class ActivityProcessor(object):
|
||||
if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'):
|
||||
logging_enabled = True
|
||||
else:
|
||||
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
||||
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
||||
logger.debug(u"Tautulli ActivityProcessor :: Session %s ratingKey %s not logged. "
|
||||
u"Does not meet logging criteria. Media type is '%s'" %
|
||||
(session['session_key'], session['rating_key'], session['media_type']))
|
||||
return session['id']
|
||||
|
||||
if str(session['paused_counter']).isdigit():
|
||||
@@ -193,15 +198,16 @@ class ActivityProcessor(object):
|
||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||
logging_enabled = False
|
||||
logger.debug(u"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||
u"seconds, so we're not logging it." %
|
||||
(session['rating_key'], str(real_play_time), plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||
logger.debug(u"Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
|
||||
u"which is less than %s seconds, so we're not logging it." %
|
||||
(session['session_key'], session['rating_key'], str(real_play_time),
|
||||
plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||
if not is_import and session['media_type'] == 'track':
|
||||
if real_play_time < 15 and session['duration'] >= 30:
|
||||
logging_enabled = False
|
||||
logger.debug(u"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs, "
|
||||
logger.debug(u"Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs, "
|
||||
u"looks like it was skipped so we're not logging it" %
|
||||
(session['rating_key'], str(real_play_time)))
|
||||
(session['session_key'], session['rating_key'], str(real_play_time)))
|
||||
elif is_import and import_ignore_interval:
|
||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||
(real_play_time < int(import_ignore_interval)):
|
||||
@@ -394,6 +400,7 @@ class ActivityProcessor(object):
|
||||
'title': session['title'],
|
||||
'parent_title': session['parent_title'],
|
||||
'grandparent_title': session['grandparent_title'],
|
||||
'original_title': session['original_title'],
|
||||
'full_title': session['full_title'],
|
||||
'media_index': metadata['media_index'],
|
||||
'parent_media_index': metadata['parent_media_index'],
|
||||
|
@@ -37,6 +37,8 @@ import logger
|
||||
import mobile_app
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import newsletter_handler
|
||||
import newsletters
|
||||
import users
|
||||
|
||||
|
||||
@@ -443,6 +445,51 @@ class API2:
|
||||
|
||||
return
|
||||
|
||||
def notify_newsletter(self, newsletter_id='', subject='', body='', message='', **kwargs):
|
||||
""" Send a newsletter using Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The ID number of the newsletter agent
|
||||
|
||||
Optional parameters:
|
||||
subject (str): The subject of the newsletter
|
||||
body (str): The body of the newsletter
|
||||
message (str): The message of the newsletter
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
if not newsletter_id:
|
||||
self._api_msg = 'Newsletter failed: no newsletter id provided.'
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||
|
||||
if not newsletter:
|
||||
self._api_msg = 'Newsletter failed: invalid newsletter_id provided %s.' % newsletter_id
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
logger.api_debug(u'Tautulli APIv2 :: Sending newsletter.')
|
||||
success = newsletter_handler.notify(newsletter_id=newsletter_id,
|
||||
notify_action='api',
|
||||
subject=subject,
|
||||
body=body,
|
||||
message=message,
|
||||
**kwargs)
|
||||
|
||||
if success:
|
||||
self._api_msg = 'Newsletter sent.'
|
||||
self._api_result_type = 'success'
|
||||
else:
|
||||
self._api_msg = 'Newsletter failed.'
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return
|
||||
|
||||
def _api_make_md(self):
|
||||
""" Tries to make a API.md to simplify the api docs. """
|
||||
|
||||
@@ -564,6 +611,7 @@ General optional parameters:
|
||||
# if we fail to generate the output fake an error
|
||||
except Exception as e:
|
||||
logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc())
|
||||
cherrypy.response.status = 500
|
||||
out['message'] = traceback.format_exc()
|
||||
out['result'] = 'error'
|
||||
|
||||
@@ -573,6 +621,7 @@ General optional parameters:
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
except Exception as e:
|
||||
logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result')
|
||||
cherrypy.response.status = 500
|
||||
try:
|
||||
out['message'] = e
|
||||
out['result'] = 'error'
|
||||
@@ -613,6 +662,7 @@ General optional parameters:
|
||||
result = call(**self._api_kwargs)
|
||||
except Exception as e:
|
||||
logger.api_error(u'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
|
||||
cherrypy.response.status = 400
|
||||
if self._api_debug:
|
||||
cherrypy.request.show_tracebacks = True
|
||||
# Reraise the exception so the traceback hits the browser
|
||||
@@ -657,4 +707,7 @@ General optional parameters:
|
||||
if ret.get('result'):
|
||||
self._api_result_type = ret.pop('result', None)
|
||||
|
||||
if self._api_result_type == 'error':
|
||||
cherrypy.response.status = 500
|
||||
|
||||
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
||||
|
@@ -20,20 +20,22 @@ import version
|
||||
|
||||
# Identify Our Application
|
||||
PLATFORM = platform.system()
|
||||
PLATFORM_VERSION = platform.release()
|
||||
PLATFORM_RELEASE = platform.release()
|
||||
PLATFORM_VERSION = platform.version()
|
||||
PLATFORM_LINUX_DISTRO = ' '.join(x for x in platform.linux_distribution() if x)
|
||||
BRANCH = version.PLEXPY_BRANCH
|
||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||
|
||||
USER_AGENT = 'Tautulli/{} ({} {})'.format(RELEASE, PLATFORM, PLATFORM_VERSION)
|
||||
USER_AGENT = 'Tautulli/{} ({} {})'.format(RELEASE, PLATFORM, PLATFORM_RELEASE)
|
||||
|
||||
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
||||
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
||||
DEFAULT_ART = "interfaces/default/images/art.png"
|
||||
|
||||
ONLINE_POSTER_THUMB = "http://tautulli.com/images/poster.png"
|
||||
ONLINE_COVER_THUMB = "http://tautulli.com/images/cover.png"
|
||||
ONLINE_ART = "http://tautulli.com/images/art.png"
|
||||
ONLINE_POSTER_THUMB = "https://tautulli.com/images/poster.png"
|
||||
ONLINE_COVER_THUMB = "https://tautulli.com/images/cover.png"
|
||||
ONLINE_ART = "https://tautulli.com/images/art.png"
|
||||
|
||||
MEDIA_TYPE_HEADERS = {
|
||||
'movie': 'Movies',
|
||||
@@ -337,6 +339,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version', 'description': 'If the stream is an optimized version.', 'example': '0 or 1'},
|
||||
{'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', 'description': 'The optimized version profile of the stream.'},
|
||||
{'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', 'description': 'If the stream is an synced version.', 'example': '0 or 1'},
|
||||
{'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.', 'example': '0 or 1'},
|
||||
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'},
|
||||
{'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', 'description': 'The network location of the stream.', 'example': 'lan or wan'},
|
||||
{'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The required bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'},
|
||||
@@ -400,6 +403,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'},
|
||||
{'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'},
|
||||
{'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'},
|
||||
{'name': 'Track Artist', 'type': 'str', 'value': 'track_artist', 'description': 'The name of the artist of the track.'},
|
||||
{'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.', 'example': 'e.g. 1, or 1-3'},
|
||||
{'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00', 'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'},
|
||||
{'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', 'example': 'e.g. 6, or 6-10'},
|
||||
@@ -519,13 +523,17 @@ NEWSLETTER_PARAMETERS = [
|
||||
'category': 'Global',
|
||||
'parameters': [
|
||||
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
|
||||
{'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newesletter.'},
|
||||
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newesletter.'},
|
||||
{'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'},
|
||||
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'},
|
||||
{'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year.'},
|
||||
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'},
|
||||
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'},
|
||||
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'},
|
||||
{'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url', 'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'},
|
||||
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'},
|
||||
{'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', 'description': 'The unique ID number for the newsletter agent.'},
|
||||
{'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', 'description': 'The unique ID name for the newsletter agent.'},
|
||||
{'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password', 'description': 'The password required to view the newsletter if enabled.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@@ -312,9 +312,14 @@ _CONFIG_DEFINITIONS = {
|
||||
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
|
||||
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
|
||||
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
|
||||
'NEWSLETTER_AUTH': (int, 'Newsletter', 0),
|
||||
'NEWSLETTER_PASSWORD': (str, 'Newsletter', ''),
|
||||
'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
|
||||
'NEWSLETTER_INLINE_STYLES': (int, 'Newsletter', 1),
|
||||
'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),
|
||||
'NEWSLETTER_DIR': (str, 'Newsletter', ''),
|
||||
'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0),
|
||||
'NEWSLETTER_STATIC_URL': (int, 'Newsletter', 0),
|
||||
'NMA_APIKEY': (str, 'NMA', ''),
|
||||
'NMA_ENABLED': (int, 'NMA', 0),
|
||||
'NMA_PRIORITY': (int, 'NMA', 0),
|
||||
|
@@ -86,6 +86,7 @@ class DataFactory(object):
|
||||
'session_history_metadata.title',
|
||||
'session_history_metadata.parent_title',
|
||||
'session_history_metadata.grandparent_title',
|
||||
'session_history_metadata.original_title',
|
||||
'session_history_metadata.year',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
@@ -132,6 +133,7 @@ class DataFactory(object):
|
||||
'title',
|
||||
'parent_title',
|
||||
'grandparent_title',
|
||||
'original_title',
|
||||
'year',
|
||||
'media_index',
|
||||
'parent_media_index',
|
||||
@@ -233,6 +235,7 @@ class DataFactory(object):
|
||||
'title': item['parent_title'],
|
||||
'parent_title': item['parent_title'],
|
||||
'grandparent_title': item['grandparent_title'],
|
||||
'original_title': item['original_title'],
|
||||
'year': item['year'],
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
@@ -480,7 +483,8 @@ class DataFactory(object):
|
||||
elif stat == 'top_music':
|
||||
top_music = []
|
||||
try:
|
||||
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
|
||||
query = 'SELECT t.id, t.grandparent_title, t.original_title, ' \
|
||||
't.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
|
||||
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
|
||||
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
|
||||
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
|
||||
@@ -492,7 +496,7 @@ class DataFactory(object):
|
||||
' >= datetime("now", "-%s days", "localtime") ' \
|
||||
' AND session_history.media_type = "track" ' \
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.grandparent_title ' \
|
||||
'GROUP BY t.original_title, t.grandparent_title ' \
|
||||
'ORDER BY %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
result = monitor_db.select(query)
|
||||
@@ -501,7 +505,7 @@ class DataFactory(object):
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
row = {'title': item['grandparent_title'],
|
||||
row = {'title': item['original_title'] or item['grandparent_title'],
|
||||
'total_plays': item['total_plays'],
|
||||
'total_duration': item['total_duration'],
|
||||
'users_watched': '',
|
||||
@@ -529,7 +533,8 @@ class DataFactory(object):
|
||||
elif stat == 'popular_music':
|
||||
popular_music = []
|
||||
try:
|
||||
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
|
||||
query = 'SELECT t.id, t.grandparent_title, t.original_title, ' \
|
||||
't.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
|
||||
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
|
||||
'COUNT(DISTINCT t.user_id) AS users_watched, ' \
|
||||
'MAX(t.started) AS last_watch, COUNT(t.id) as total_plays, SUM(t.d) AS total_duration ' \
|
||||
@@ -542,7 +547,7 @@ class DataFactory(object):
|
||||
' >= datetime("now", "-%s days", "localtime") ' \
|
||||
' AND session_history.media_type = "track" ' \
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.grandparent_title ' \
|
||||
'GROUP BY t.original_title, t.grandparent_title ' \
|
||||
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
result = monitor_db.select(query)
|
||||
@@ -551,7 +556,7 @@ class DataFactory(object):
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
row = {'title': item['grandparent_title'],
|
||||
row = {'title': item['original_title'] or item['grandparent_title'],
|
||||
'users_watched': item['users_watched'],
|
||||
'rating_key': item['grandparent_rating_key'],
|
||||
'last_play': item['last_watch'],
|
||||
@@ -888,7 +893,7 @@ class DataFactory(object):
|
||||
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
|
||||
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
|
||||
'transcode_width, transcode_height, ' \
|
||||
'session_history_metadata.media_type, title, grandparent_title ' \
|
||||
'session_history_metadata.media_type, title, grandparent_title, original_title ' \
|
||||
'FROM session_history_media_info ' \
|
||||
'JOIN session_history ON session_history_media_info.id = session_history.id ' \
|
||||
'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id ' \
|
||||
@@ -909,7 +914,7 @@ class DataFactory(object):
|
||||
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
|
||||
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
|
||||
'transcode_width, transcode_height, ' \
|
||||
'media_type, title, grandparent_title ' \
|
||||
'media_type, title, grandparent_title, original_title ' \
|
||||
'FROM sessions ' \
|
||||
'WHERE session_key = ? %s' % user_cond
|
||||
result = monitor_db.select(query, args=[session_key])
|
||||
@@ -979,6 +984,7 @@ class DataFactory(object):
|
||||
'media_type': item['media_type'],
|
||||
'title': item['title'],
|
||||
'grandparent_title': item['grandparent_title'],
|
||||
'original_title': item['original_title'],
|
||||
'current_session': 1 if session_key else 0,
|
||||
'pre_tautulli': pre_tautulli
|
||||
}
|
||||
@@ -994,7 +1000,8 @@ class DataFactory(object):
|
||||
'session_history_metadata.rating_key, session_history_metadata.parent_rating_key, ' \
|
||||
'session_history_metadata.grandparent_rating_key, session_history_metadata.title, ' \
|
||||
'session_history_metadata.parent_title, session_history_metadata.grandparent_title, ' \
|
||||
'session_history_metadata.full_title, library_sections.section_name, ' \
|
||||
'session_history_metadata.original_title, session_history_metadata.full_title, ' \
|
||||
'library_sections.section_name, ' \
|
||||
'session_history_metadata.media_index, session_history_metadata.parent_media_index, ' \
|
||||
'session_history_metadata.section_id, session_history_metadata.thumb, ' \
|
||||
'session_history_metadata.parent_thumb, session_history_metadata.grandparent_thumb, ' \
|
||||
@@ -1043,6 +1050,7 @@ class DataFactory(object):
|
||||
'parent_rating_key': item['parent_rating_key'],
|
||||
'grandparent_rating_key': item['grandparent_rating_key'],
|
||||
'grandparent_title': item['grandparent_title'],
|
||||
'original_title': item['original_title'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'parent_title': item['parent_title'],
|
||||
'media_index': item['media_index'],
|
||||
@@ -1215,52 +1223,64 @@ class DataFactory(object):
|
||||
|
||||
monitor_db.upsert(table, key_dict=keys, value_dict=values)
|
||||
|
||||
def delete_img_info(self, rating_key=None, service=None):
|
||||
def delete_img_info(self, rating_key=None, service='', delete_all=False):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if rating_key:
|
||||
service = service or helpers.get_img_service()
|
||||
if not delete_all:
|
||||
service = helpers.get_img_service()
|
||||
|
||||
if service == 'imgur':
|
||||
# Delete from Imgur
|
||||
query = 'SELECT imgur_title, delete_hash, fallback FROM imgur_lookup ' \
|
||||
'JOIN image_hash_lookup ON imgur_lookup.img_hash = image_hash_lookup.img_hash ' \
|
||||
'WHERE rating_key = ? '
|
||||
args = [rating_key]
|
||||
results = monitor_db.select(query, args=args)
|
||||
|
||||
for imgur_info in results:
|
||||
if imgur_info['delete_hash']:
|
||||
helpers.delete_from_imgur(delete_hash=imgur_info['delete_hash'],
|
||||
img_title=imgur_info['imgur_title'],
|
||||
fallback=imgur_info['fallback'])
|
||||
|
||||
logger.info(u"Tautulli DataFactory :: Deleting Imgur info for rating_key %s from the database."
|
||||
% rating_key)
|
||||
result = monitor_db.action('DELETE FROM imgur_lookup WHERE img_hash '
|
||||
'IN (SELECT img_hash FROM image_hash_lookup WHERE rating_key = ?)',
|
||||
[rating_key])
|
||||
|
||||
elif service == 'cloudinary':
|
||||
# Delete from Cloudinary
|
||||
helpers.delete_from_cloudinary(rating_key=rating_key)
|
||||
|
||||
logger.info(u"Tautulli DataFactory :: Deleting Cloudinary info for rating_key %s from the database."
|
||||
% rating_key)
|
||||
result = monitor_db.action('DELETE FROM cloudinary_lookup WHERE img_hash '
|
||||
'IN (SELECT img_hash FROM image_hash_lookup WHERE rating_key = ?)',
|
||||
[rating_key])
|
||||
|
||||
else:
|
||||
logger.error(u"Tautulli DataFactory :: Unable to delete hosted images: invalid service '%s' provided."
|
||||
% service)
|
||||
|
||||
return service
|
||||
|
||||
else:
|
||||
if not rating_key and not delete_all:
|
||||
logger.error(u"Tautulli DataFactory :: Unable to delete hosted images: rating_key not provided.")
|
||||
return False
|
||||
|
||||
where = ''
|
||||
args = []
|
||||
log_msg = ''
|
||||
if rating_key:
|
||||
where = 'WHERE rating_key = ?'
|
||||
args = [rating_key]
|
||||
log_msg = ' for rating_key %s' % rating_key
|
||||
|
||||
if service.lower() == 'imgur':
|
||||
# Delete from Imgur
|
||||
query = 'SELECT imgur_title, delete_hash, fallback FROM imgur_lookup ' \
|
||||
'JOIN image_hash_lookup ON imgur_lookup.img_hash = image_hash_lookup.img_hash %s' % where
|
||||
results = monitor_db.select(query, args=args)
|
||||
|
||||
for imgur_info in results:
|
||||
if imgur_info['delete_hash']:
|
||||
helpers.delete_from_imgur(delete_hash=imgur_info['delete_hash'],
|
||||
img_title=imgur_info['imgur_title'],
|
||||
fallback=imgur_info['fallback'])
|
||||
|
||||
logger.info(u"Tautulli DataFactory :: Deleting Imgur info%s from the database."
|
||||
% log_msg)
|
||||
result = monitor_db.action('DELETE FROM imgur_lookup WHERE img_hash '
|
||||
'IN (SELECT img_hash FROM image_hash_lookup %s)' % where,
|
||||
args)
|
||||
|
||||
elif service.lower() == 'cloudinary':
|
||||
# Delete from Cloudinary
|
||||
query = 'SELECT cloudinary_title, rating_key, fallback FROM cloudinary_lookup ' \
|
||||
'JOIN image_hash_lookup ON cloudinary_lookup.img_hash = image_hash_lookup.img_hash %s ' \
|
||||
'GROUP BY rating_key' % where
|
||||
results = monitor_db.select(query, args=args)
|
||||
|
||||
for cloudinary_info in results:
|
||||
helpers.delete_from_cloudinary(rating_key=cloudinary_info['rating_key'])
|
||||
|
||||
logger.info(u"Tautulli DataFactory :: Deleting Cloudinary info%s from the database."
|
||||
% log_msg)
|
||||
result = monitor_db.action('DELETE FROM cloudinary_lookup WHERE img_hash '
|
||||
'IN (SELECT img_hash FROM image_hash_lookup %s)' % where,
|
||||
args)
|
||||
|
||||
else:
|
||||
logger.error(u"Tautulli DataFactory :: Unable to delete hosted images: invalid service '%s' provided."
|
||||
% service)
|
||||
|
||||
return service
|
||||
|
||||
def get_poster_info(self, rating_key='', metadata=None, service=None):
|
||||
poster_key = ''
|
||||
if str(rating_key).isdigit():
|
||||
@@ -1538,8 +1558,11 @@ class DataFactory(object):
|
||||
|
||||
if metadata:
|
||||
# Create full_title
|
||||
if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track':
|
||||
if metadata['media_type'] == 'episode':
|
||||
full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||
elif metadata['media_type'] == 'track':
|
||||
full_title = '%s - %s' % (metadata['title'],
|
||||
metadata['original_title'] or metadata['grandparent_title'])
|
||||
else:
|
||||
full_title = metadata['title']
|
||||
|
||||
@@ -1554,7 +1577,8 @@ class DataFactory(object):
|
||||
|
||||
# Update the session_history_metadata table
|
||||
query = 'UPDATE session_history_metadata SET rating_key = ?, parent_rating_key = ?, ' \
|
||||
'grandparent_rating_key = ?, title = ?, parent_title = ?, grandparent_title = ?, full_title = ?, ' \
|
||||
'grandparent_rating_key = ?, title = ?, parent_title = ?, grandparent_title = ?, ' \
|
||||
'original_title = ?, full_title = ?, ' \
|
||||
'media_index = ?, parent_media_index = ?, section_id = ?, thumb = ?, parent_thumb = ?, ' \
|
||||
'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \
|
||||
'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \
|
||||
@@ -1563,7 +1587,8 @@ class DataFactory(object):
|
||||
'WHERE rating_key = ?'
|
||||
|
||||
args = [metadata['rating_key'], metadata['parent_rating_key'], metadata['grandparent_rating_key'],
|
||||
metadata['title'], metadata['parent_title'], metadata['grandparent_title'], full_title,
|
||||
metadata['title'], metadata['parent_title'], metadata['grandparent_title'],
|
||||
metadata['original_title'], full_title,
|
||||
metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'],
|
||||
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
|
||||
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
||||
|
@@ -209,6 +209,9 @@ def now():
|
||||
now = datetime.datetime.now()
|
||||
return now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def utc_now_iso():
|
||||
utcnow = datetime.datetime.utcnow()
|
||||
return utcnow.isoformat()
|
||||
|
||||
def human_duration(s, sig='dhms'):
|
||||
|
||||
@@ -792,8 +795,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
||||
try:
|
||||
response = upload('data:image/png;base64,{}'.format(base64.b64encode(img_data)),
|
||||
public_id='{}_{}'.format(fallback, rating_key),
|
||||
tags=[fallback, rating_key],
|
||||
context={'title': img_title, 'rating_key': rating_key, 'fallback': fallback})
|
||||
tags=[fallback, str(rating_key)],
|
||||
context={'title': img_title.encode('utf-8'), 'rating_key': str(rating_key), 'fallback': fallback})
|
||||
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback))
|
||||
img_url = response.get('url', '')
|
||||
except Exception as e:
|
||||
@@ -834,13 +837,17 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
|
||||
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
|
||||
)
|
||||
|
||||
img_options = {}
|
||||
img_options = {'format': img_format,
|
||||
'fetch_format': 'auto',
|
||||
'quality': 'auto',
|
||||
'version': int(time.time()),
|
||||
'secure': True}
|
||||
|
||||
if width != 1000:
|
||||
img_options['width'] = width
|
||||
img_options['width'] = str(width)
|
||||
img_options['crop'] = 'fill'
|
||||
if height != 1500:
|
||||
img_options['height'] = height
|
||||
img_options['height'] = str(height)
|
||||
img_options['crop'] = 'fill'
|
||||
if opacity != 100:
|
||||
img_options['opacity'] = opacity
|
||||
@@ -849,14 +856,11 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
|
||||
if blur != 0:
|
||||
img_options['effect'] = 'blur:{}'.format(blur * 100)
|
||||
|
||||
if img_options:
|
||||
img_options['format'] = img_format
|
||||
|
||||
try:
|
||||
url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options)
|
||||
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback))
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e))
|
||||
try:
|
||||
url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options)
|
||||
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback))
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e))
|
||||
|
||||
return url
|
||||
|
||||
@@ -949,7 +953,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
"""
|
||||
valid_tokens = re.compile(r'(\(|\)|and|or)')
|
||||
conditions_pattern = re.compile(r'{\d+}')
|
||||
|
||||
|
||||
tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()]
|
||||
|
||||
stack = [[]]
|
||||
@@ -960,7 +964,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
close_bracket_next = False
|
||||
nest_and = 0
|
||||
nest_nest_and = 0
|
||||
|
||||
|
||||
for i, x in enumerate(tokens):
|
||||
if open_bracket_next and x == '(':
|
||||
stack[-1].append([])
|
||||
@@ -971,7 +975,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
close_bracket_next = False
|
||||
if nest_and:
|
||||
nest_nest_and += 1
|
||||
|
||||
|
||||
elif close_bracket_next and x == ')':
|
||||
stack.pop()
|
||||
if not stack:
|
||||
@@ -1000,7 +1004,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
if nest_and > nest_nest_and:
|
||||
stack.pop()
|
||||
nest_and -= 1
|
||||
|
||||
|
||||
elif bool_next and x == 'and' and i < len(tokens)-1:
|
||||
stack[-1].append([])
|
||||
stack.append(stack[-1][-1])
|
||||
@@ -1011,7 +1015,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
open_bracket_next = True
|
||||
close_bracket_next = False
|
||||
nest_and += 1
|
||||
|
||||
|
||||
elif bool_next and x == 'or' and i < len(tokens)-1:
|
||||
stack[-1].append(x)
|
||||
cond_next = True
|
||||
@@ -1072,7 +1076,10 @@ def get_plexpy_url(hostname=None):
|
||||
s.connect(('<broadcast>', 0))
|
||||
hostname = s.getsockname()[0]
|
||||
except socket.error:
|
||||
hostname = socket.gethostbyname(socket.gethostname())
|
||||
try:
|
||||
hostname = socket.gethostbyname(socket.gethostname())
|
||||
except socket.gaierror:
|
||||
pass
|
||||
|
||||
if not hostname:
|
||||
hostname = 'localhost'
|
||||
|
@@ -43,7 +43,7 @@ class HTTPHandler(object):
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': plexpy.common.RELEASE,
|
||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
}
|
||||
|
||||
|
@@ -862,13 +862,13 @@ class Libraries(object):
|
||||
if str(section_id).isdigit():
|
||||
query = 'SELECT session_history.id, session_history.media_type, ' \
|
||||
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
|
||||
'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
|
||||
'title, parent_title, grandparent_title, original_title, ' \
|
||||
'thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
|
||||
'year, started, user, content_rating, labels, section_id ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE section_id = ? ' \
|
||||
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
|
||||
' ELSE session_history.rating_key END) ' \
|
||||
'GROUP BY session_history.rating_key ' \
|
||||
'ORDER BY started DESC LIMIT ?'
|
||||
result = monitor_db.select(query, args=[section_id, limit])
|
||||
else:
|
||||
@@ -893,6 +893,7 @@ class Libraries(object):
|
||||
'title': row['title'],
|
||||
'parent_title': row['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': row['original_title'],
|
||||
'thumb': thumb,
|
||||
'media_index': row['media_index'],
|
||||
'parent_media_index': row['parent_media_index'],
|
||||
|
@@ -86,7 +86,9 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
|
||||
body = newsletter_config['body']
|
||||
message = newsletter_config['message']
|
||||
|
||||
newsletter_agent = newsletters.get_agent_class(agent_id=newsletter_config['agent_id'],
|
||||
newsletter_agent = newsletters.get_agent_class(newsletter_id=newsletter_id,
|
||||
newsletter_id_name=newsletter_config['id_name'],
|
||||
agent_id=newsletter_config['agent_id'],
|
||||
config=newsletter_config['config'],
|
||||
email_config=newsletter_config['email_config'],
|
||||
subject=subject,
|
||||
@@ -100,6 +102,7 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
|
||||
subject=newsletter_agent.subject_formatted,
|
||||
body=newsletter_agent.body_formatted,
|
||||
message=newsletter_agent.message_formatted,
|
||||
filename=newsletter_agent.filename_formatted,
|
||||
start_date=newsletter_agent.start_date.format('YYYY-MM-DD'),
|
||||
end_date=newsletter_agent.end_date.format('YYYY-MM-DD'),
|
||||
start_time=newsletter_agent.start_time,
|
||||
@@ -114,7 +117,7 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
|
||||
return True
|
||||
|
||||
|
||||
def set_notify_state(newsletter, notify_action, subject, body, message,
|
||||
def set_notify_state(newsletter, notify_action, subject, body, message, filename,
|
||||
start_date, end_date, start_time, end_time, newsletter_uuid):
|
||||
|
||||
if newsletter and notify_action:
|
||||
@@ -133,7 +136,8 @@ def set_notify_state(newsletter, notify_action, subject, body, message,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time}
|
||||
'end_time': end_time,
|
||||
'filename': filename}
|
||||
|
||||
db.upsert(table_name='newsletter_log', key_dict=keys, value_dict=values)
|
||||
return db.last_insert_id()
|
||||
@@ -149,20 +153,29 @@ def set_notify_success(newsletter_log_id):
|
||||
db.upsert(table_name='newsletter_log', key_dict=keys, value_dict=values)
|
||||
|
||||
|
||||
def get_newsletter(newsletter_uuid):
|
||||
def get_newsletter(newsletter_uuid=None, newsletter_id_name=None):
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select_single('SELECT newsletter_id, start_date, end_date FROM newsletter_log '
|
||||
'WHERE uuid = ?', [newsletter_uuid])
|
||||
|
||||
if newsletter_uuid:
|
||||
result = db.select_single('SELECT start_date, end_date, uuid, filename FROM newsletter_log '
|
||||
'WHERE uuid = ?', [newsletter_uuid])
|
||||
elif newsletter_id_name:
|
||||
result = db.select_single('SELECT start_date, end_date, uuid, filename FROM newsletter_log '
|
||||
'JOIN newsletters ON newsletters.id = newsletter_log.newsletter_id '
|
||||
'WHERE id_name = ? AND notify_action != "test" '
|
||||
'ORDER BY timestamp DESC LIMIT 1', [newsletter_id_name])
|
||||
else:
|
||||
result = None
|
||||
|
||||
if result:
|
||||
newsletter_id = result['newsletter_id']
|
||||
newsletter_uuid = result['uuid']
|
||||
start_date = result['start_date']
|
||||
end_date = result['end_date']
|
||||
newsletter_file = result['filename'] or 'newsletter_%s-%s_%s.html' % (start_date.replace('-', ''),
|
||||
end_date.replace('-', ''),
|
||||
newsletter_uuid)
|
||||
|
||||
newsletter_file = 'newsletter_%s-%s_%s.html' % (start_date.replace('-', ''),
|
||||
end_date.replace('-', ''),
|
||||
newsletter_uuid)
|
||||
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR
|
||||
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR or os.path.join(plexpy.DATA_DIR, 'newsletters')
|
||||
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
|
||||
|
||||
if newsletter_file in os.listdir(newsletter_folder):
|
||||
@@ -173,4 +186,4 @@ def get_newsletter(newsletter_uuid):
|
||||
except OSError as e:
|
||||
logger.error(u"Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e))
|
||||
else:
|
||||
logger.warn(u"Tautulli NewsletterHandler :: Newsletter '%s' file is missing." % newsletter_uuid)
|
||||
logger.warn(u"Tautulli NewsletterHandler :: Newsletter file '%s' is missing." % newsletter_file)
|
||||
|
@@ -19,6 +19,7 @@ from itertools import groupby
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
import os
|
||||
import re
|
||||
|
||||
import plexpy
|
||||
import common
|
||||
@@ -63,12 +64,14 @@ def available_notification_actions():
|
||||
return actions
|
||||
|
||||
|
||||
def get_agent_class(agent_id=None, config=None, email_config=None, start_date=None, end_date=None,
|
||||
subject=None, body=None, message=None):
|
||||
def get_agent_class(newsletter_id=None, newsletter_id_name=None, agent_id=None, config=None, email_config=None,
|
||||
start_date=None, end_date=None, subject=None, body=None, message=None):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
|
||||
kwargs = {'config': config,
|
||||
kwargs = {'newsletter_id': newsletter_id,
|
||||
'newsletter_id_name': newsletter_id_name,
|
||||
'config': config,
|
||||
'email_config': email_config,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
@@ -138,7 +141,9 @@ def get_newsletter_config(newsletter_id=None):
|
||||
subject = result.pop('subject')
|
||||
body = result.pop('body')
|
||||
message = result.pop('message')
|
||||
newsletter_agent = get_agent_class(agent_id=result['agent_id'], config=config, email_config=email_config,
|
||||
newsletter_agent = get_agent_class(newsletter_id=newsletter_id, newsletter_id_name=result['id_name'],
|
||||
agent_id=result['agent_id'],
|
||||
config=config, email_config=email_config,
|
||||
subject=subject, body=body, message=message)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e)
|
||||
@@ -176,6 +181,7 @@ def add_newsletter_config(agent_id=None, **kwargs):
|
||||
values = {'agent_id': agent['id'],
|
||||
'agent_name': agent['name'],
|
||||
'agent_label': agent['label'],
|
||||
'id_name': '',
|
||||
'friendly_name': '',
|
||||
'newsletter_config': json.dumps(agent_class.config),
|
||||
'email_config': json.dumps(agent_class.email_config),
|
||||
@@ -200,7 +206,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
else:
|
||||
logger.error(u"Tautulli Newsletters :: Unable to set exisiting newsletter: invalid agent_id %s."
|
||||
logger.error(u"Tautulli Newsletters :: Unable to set existing newsletter: invalid agent_id %s."
|
||||
% agent_id)
|
||||
return False
|
||||
|
||||
@@ -223,13 +229,15 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
||||
body = kwargs.pop('body')
|
||||
message = kwargs.pop('message')
|
||||
|
||||
agent_class = get_agent_class(agent_id=agent['id'], config=newsletter_config, email_config=email_config,
|
||||
agent_class = get_agent_class(agent_id=agent['id'],
|
||||
config=newsletter_config, email_config=email_config,
|
||||
subject=subject, body=body, message=message)
|
||||
|
||||
keys = {'id': newsletter_id}
|
||||
values = {'agent_id': agent['id'],
|
||||
'agent_name': agent['name'],
|
||||
'agent_label': agent['label'],
|
||||
'id_name': kwargs.get('id_name', ''),
|
||||
'friendly_name': kwargs.get('friendly_name', ''),
|
||||
'newsletter_config': json.dumps(agent_class.config),
|
||||
'email_config': json.dumps(agent_class.email_config),
|
||||
@@ -267,16 +275,22 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
|
||||
|
||||
|
||||
def serve_template(templatename, **kwargs):
|
||||
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
||||
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES)
|
||||
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
|
||||
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
||||
else:
|
||||
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
||||
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES)
|
||||
|
||||
if not plexpy.CONFIG.NEWSLETTER_INLINE_STYLES:
|
||||
templatename = templatename.replace('.html', '.internal.html')
|
||||
|
||||
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
|
||||
|
||||
try:
|
||||
template = _hplookup.get_template(templatename)
|
||||
return template.render(**kwargs)
|
||||
return template.render(**kwargs), False
|
||||
except:
|
||||
return exceptions.html_error_template().render()
|
||||
return exceptions.html_error_template().render(), True
|
||||
|
||||
|
||||
def generate_newsletter_uuid():
|
||||
@@ -299,22 +313,26 @@ class Newsletter(object):
|
||||
'time_frame': 7,
|
||||
'time_frame_units': 'days',
|
||||
'formatted': 1,
|
||||
'notifier_id': 0}
|
||||
'notifier_id': 0,
|
||||
'filename': '',
|
||||
'save_only': 0}
|
||||
_DEFAULT_EMAIL_CONFIG = EMAIL().return_default_config()
|
||||
_DEFAULT_EMAIL_CONFIG['from_name'] = 'Tautulli Newsletter'
|
||||
_DEFAULT_EMAIL_CONFIG['notifier_id'] = 0
|
||||
_DEFAULT_SUBJECT = 'Tautulli Newsletter'
|
||||
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
|
||||
_DEFAULT_MESSAGE = ''
|
||||
_TEMPLATE_MASTER = ''
|
||||
_DEFAULT_FILENAME = 'newsletter_{newsletter_uuid}.html'
|
||||
_TEMPLATE = ''
|
||||
|
||||
def __init__(self, config=None, email_config=None, start_date=None, end_date=None,
|
||||
subject=None, body=None, message=None):
|
||||
def __init__(self, newsletter_id=None, newsletter_id_name=None, config=None, email_config=None,
|
||||
start_date=None, end_date=None, subject=None, body=None, message=None):
|
||||
self.config = self.set_config(config=config, default=self._DEFAULT_CONFIG)
|
||||
self.email_config = self.set_config(config=email_config, default=self._DEFAULT_EMAIL_CONFIG)
|
||||
self.uuid = generate_newsletter_uuid()
|
||||
|
||||
self.newsletter_id = newsletter_id
|
||||
self.newsletter_id_name = newsletter_id_name or ''
|
||||
self.start_date = None
|
||||
self.end_date = None
|
||||
|
||||
@@ -346,12 +364,19 @@ class Newsletter(object):
|
||||
self.subject = subject or self._DEFAULT_SUBJECT
|
||||
self.body = body or self._DEFAULT_BODY
|
||||
self.message = message or self._DEFAULT_MESSAGE
|
||||
self.filename = self.config['filename'] or self._DEFAULT_FILENAME
|
||||
|
||||
if not self.filename.endswith('.html'):
|
||||
self.filename += '.html'
|
||||
|
||||
self.subject_formatted, self.body_formatted, self.message_formatted = self.build_text()
|
||||
self.filename_formatted = self.build_filename()
|
||||
|
||||
self.data = {}
|
||||
self.newsletter = None
|
||||
|
||||
self.is_preview = False
|
||||
self.template_error = None
|
||||
|
||||
def set_config(self, config=None, default=None):
|
||||
return self._validate_config(config=config, default=default)
|
||||
@@ -391,19 +416,14 @@ class Newsletter(object):
|
||||
'parameters': self.parameters,
|
||||
'data': self.data}
|
||||
|
||||
def generate_newsletter(self, preview=False, master=False):
|
||||
def generate_newsletter(self, preview=False):
|
||||
if preview:
|
||||
self.is_preview = True
|
||||
|
||||
if master:
|
||||
template = self._TEMPLATE_MASTER
|
||||
else:
|
||||
template = self._TEMPLATE
|
||||
|
||||
self.retrieve_data()
|
||||
|
||||
return serve_template(
|
||||
templatename=template,
|
||||
newsletter_rendered, self.template_error = serve_template(
|
||||
templatename=self._TEMPLATE,
|
||||
uuid=self.uuid,
|
||||
subject=self.subject_formatted,
|
||||
body=self.body_formatted,
|
||||
@@ -413,21 +433,50 @@ class Newsletter(object):
|
||||
preview=self.is_preview
|
||||
)
|
||||
|
||||
if self.template_error:
|
||||
return newsletter_rendered
|
||||
|
||||
# Force Tautulli footer
|
||||
if '<!-- FOOTER MESSAGE - DO NOT REMOVE -->' in newsletter_rendered:
|
||||
newsletter_rendered = newsletter_rendered.replace(
|
||||
'<!-- FOOTER MESSAGE - DO NOT REMOVE -->',
|
||||
'Newsletter generated by <a href="https://tautulli.com" target="_blank" '
|
||||
'style="text-decoration: underline;color: inherit;font-size: inherit;">Tautulli</a>.'
|
||||
)
|
||||
|
||||
else:
|
||||
msg = ('<div style="text-align: center;padding-top: 100px;padding-bottom: 100px;">'
|
||||
'<p style="font-family: \'Open Sans\', Helvetica, Arial, sans-serif;color: #282A2D;'
|
||||
'font-size: 18px;line-height: 30px;">'
|
||||
'The Tautulli newsletter footer was removed from the newsletter template.<br>'
|
||||
'Please leave the footer in place as it is unobtrusive and supports '
|
||||
'<a href="https://tautulli.com" target="_blank">Tautulli</a>.<br>Thank you.'
|
||||
'</p></div>')
|
||||
newsletter_rendered = re.sub(r'(<body.*?>)', r'\1' + msg, newsletter_rendered)
|
||||
|
||||
return newsletter_rendered
|
||||
|
||||
def send(self):
|
||||
self.newsletter = self.generate_newsletter()
|
||||
|
||||
if self.template_error:
|
||||
logger.error(u"Tautulli Newsletters :: %s newsletter failed to render template. Newsletter not sent." % self.NAME)
|
||||
return False
|
||||
|
||||
if not self._has_data():
|
||||
logger.warn(u"Tautulli Newsletters :: %s newsletter has no data. Newsletter not sent." % self.NAME)
|
||||
return False
|
||||
|
||||
self._save()
|
||||
|
||||
if self.config['save_only']:
|
||||
return True
|
||||
|
||||
return self._send()
|
||||
|
||||
def _save(self):
|
||||
newsletter_file = 'newsletter_%s-%s_%s.html' % (self.start_date.format('YYYYMMDD'),
|
||||
self.end_date.format('YYYYMMDD'),
|
||||
self.uuid)
|
||||
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR
|
||||
newsletter_file = self.filename_formatted
|
||||
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR or os.path.join(plexpy.DATA_DIR, 'newsletters')
|
||||
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
|
||||
|
||||
# In case the user has deleted it manually
|
||||
@@ -439,26 +488,35 @@ class Newsletter(object):
|
||||
for line in self.newsletter.encode('utf-8').splitlines():
|
||||
if '<!-- IGNORE SAVE -->' not in line:
|
||||
n_file.write(line + '\r\n')
|
||||
#n_file.write(line.strip())
|
||||
|
||||
logger.info(u"Tautulli Newsletters :: %s newsletter saved to %s" % (self.NAME, newsletter_file))
|
||||
logger.info(u"Tautulli Newsletters :: %s newsletter saved to '%s'" % (self.NAME, newsletter_file))
|
||||
except OSError as e:
|
||||
logger.error(u"Tautulli Newsletters :: Failed to save %s newsletter to %s: %s"
|
||||
logger.error(u"Tautulli Newsletters :: Failed to save %s newsletter to '%s': %s"
|
||||
% (self.NAME, newsletter_file, e))
|
||||
|
||||
def _send(self):
|
||||
if self.config['formatted']:
|
||||
newsletter_stripped = ''.join(l.strip() for l in self.newsletter.splitlines())
|
||||
|
||||
plaintext = 'HTML email support is required to view the newsletter.\n'
|
||||
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
plaintext += self._DEFAULT_BODY.format(**self.parameters)
|
||||
|
||||
if self.email_config['notifier_id']:
|
||||
return send_notification(
|
||||
notifier_id=self.email_config['notifier_id'],
|
||||
subject=self.subject_formatted,
|
||||
body=self.newsletter
|
||||
body=newsletter_stripped,
|
||||
plaintext=plaintext
|
||||
)
|
||||
|
||||
else:
|
||||
email = EMAIL(config=self.email_config)
|
||||
return email.notify(
|
||||
subject=self.subject_formatted,
|
||||
body=self.newsletter
|
||||
body=newsletter_stripped,
|
||||
plaintext=plaintext
|
||||
)
|
||||
elif self.config['notifier_id']:
|
||||
return send_notification(
|
||||
@@ -475,7 +533,10 @@ class Newsletter(object):
|
||||
def _build_params(self):
|
||||
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
|
||||
|
||||
base_url = plexpy.CONFIG.HTTP_BASE_URL or helpers.get_plexpy_url()
|
||||
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
|
||||
else:
|
||||
base_url = helpers.get_plexpy_url() + '/newsletter/'
|
||||
|
||||
parameters = {
|
||||
'server_name': plexpy.CONFIG.PMS_NAME,
|
||||
@@ -484,8 +545,12 @@ class Newsletter(object):
|
||||
'week_number': self.start_date.isocalendar()[1],
|
||||
'newsletter_time_frame': self.config['time_frame'],
|
||||
'newsletter_time_frame_units': self.config['time_frame_units'],
|
||||
'newsletter_url': base_url.rstrip('/') + plexpy.HTTP_ROOT + 'newsletter/' + self.uuid,
|
||||
'newsletter_uuid': self.uuid
|
||||
'newsletter_url': base_url + self.uuid,
|
||||
'newsletter_static_url': base_url + 'id/' + self.newsletter_id_name,
|
||||
'newsletter_uuid': self.uuid,
|
||||
'newsletter_id': self.newsletter_id,
|
||||
'newsletter_id_name': self.newsletter_id_name,
|
||||
'newsletter_password': plexpy.CONFIG.NEWSLETTER_PASSWORD
|
||||
}
|
||||
|
||||
return parameters
|
||||
@@ -529,6 +594,23 @@ class Newsletter(object):
|
||||
|
||||
return subject, body, message
|
||||
|
||||
def build_filename(self):
|
||||
from notification_handler import CustomFormatter
|
||||
custom_formatter = CustomFormatter()
|
||||
|
||||
try:
|
||||
filename = custom_formatter.format(unicode(self.filename), **self.parameters)
|
||||
except LookupError as e:
|
||||
logger.error(
|
||||
u"Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e)
|
||||
filename = unicode(self._DEFAULT_FILENAME).format(**self.parameters)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
u"Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
|
||||
filename = unicode(self._DEFAULT_FILENAME).format(**self.parameters)
|
||||
|
||||
return filename
|
||||
|
||||
def return_config_options(self):
|
||||
return self._return_config_options()
|
||||
|
||||
@@ -554,7 +636,6 @@ class RecentlyAdded(Newsletter):
|
||||
_DEFAULT_SUBJECT = 'Recently Added to {server_name}! ({end_date})'
|
||||
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
|
||||
_DEFAULT_MESSAGE = ''
|
||||
_TEMPLATE_MASTER = 'recently_added_master.html'
|
||||
_TEMPLATE = 'recently_added.html'
|
||||
|
||||
def _get_recently_added(self, media_type=None):
|
||||
@@ -692,7 +773,7 @@ class RecentlyAdded(Newsletter):
|
||||
artists = recently_added.get('artist', [])
|
||||
albums = [a for artist in artists for a in artist['album']]
|
||||
|
||||
if self.is_preview or plexpy.CONFIG.NEWSLETTER_SELF_HOSTED:
|
||||
if self.is_preview or helpers.get_img_service(include_self=True) == 'self-hosted':
|
||||
for item in movies + shows + albums:
|
||||
if item['media_type'] == 'album':
|
||||
height = 150
|
||||
@@ -711,10 +792,11 @@ class RecentlyAdded(Newsletter):
|
||||
else:
|
||||
item['art_hash'] = ''
|
||||
|
||||
item['poster_url'] = ''
|
||||
item['thumb_url'] = ''
|
||||
item['art_url'] = ''
|
||||
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
|
||||
|
||||
else:
|
||||
elif helpers.get_img_service():
|
||||
# Upload posters and art to image hosting service
|
||||
for item in movies + shows + albums:
|
||||
if item['media_type'] == 'album':
|
||||
@@ -728,7 +810,7 @@ class RecentlyAdded(Newsletter):
|
||||
img=item['thumb'], rating_key=item['rating_key'], title=item['title'],
|
||||
width=150, height=height, fallback=fallback)
|
||||
|
||||
item['poster_url'] = img_info.get('img_url') or common.ONLINE_POSTER_THUMB
|
||||
item['thumb_url'] = img_info.get('img_url') or common.ONLINE_POSTER_THUMB
|
||||
|
||||
img_info = get_img_info(
|
||||
img=item['art'], rating_key=item['rating_key'], title=item['title'],
|
||||
@@ -738,6 +820,15 @@ class RecentlyAdded(Newsletter):
|
||||
|
||||
item['thumb_hash'] = ''
|
||||
item['art_hash'] = ''
|
||||
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
|
||||
|
||||
else:
|
||||
for item in movies + shows + albums:
|
||||
item['thumb_hash'] = ''
|
||||
item['art_hash'] = ''
|
||||
item['thumb_url'] = ''
|
||||
item['art_url'] = ''
|
||||
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
|
||||
|
||||
self.data['recently_added'] = recently_added
|
||||
|
||||
@@ -797,4 +888,4 @@ class RecentlyAdded(Newsletter):
|
||||
}
|
||||
]
|
||||
|
||||
return config_options + additional_config
|
||||
return additional_config + config_options
|
||||
|
@@ -169,7 +169,7 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
|
||||
return stream_data['machine_id'] not in user_devices
|
||||
|
||||
elif stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode':
|
||||
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
|
||||
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
||||
|
||||
if notify_action == 'on_stop':
|
||||
@@ -256,7 +256,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
|
||||
elif parameter_type == 'float':
|
||||
values = [helpers.cast_to_float(v) for v in values]
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s', values '%s', to type '%s'."
|
||||
% (parameter, values, parameter_type))
|
||||
@@ -317,14 +317,16 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
else:
|
||||
evaluated_logic = all(evaluated_conditions[1:])
|
||||
|
||||
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
|
||||
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '{}'. Conditions: {}.".format(
|
||||
evaluated_logic, evaluated_conditions[1:]))
|
||||
|
||||
return evaluated_logic
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
|
||||
logger.info(u"Tautulli NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id)
|
||||
logger.info(u"Tautulli NotificationHandler :: Preparing notification for notifier_id %s." % notifier_id)
|
||||
|
||||
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||
|
||||
@@ -631,6 +633,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
notify_params['parent_title'])
|
||||
else:
|
||||
poster_thumb = ''
|
||||
poster_key = ''
|
||||
poster_title = ''
|
||||
|
||||
img_service = helpers.get_img_service(include_self=True)
|
||||
if img_service not in (None, 'self-hosted'):
|
||||
@@ -740,6 +744,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'optimized_version': notify_params['optimized_version'],
|
||||
'optimized_version_profile': notify_params['optimized_version_profile'],
|
||||
'synced_version': notify_params['synced_version'],
|
||||
'live': notify_params['live'],
|
||||
'stream_local': notify_params['local'],
|
||||
'stream_location': notify_params['location'],
|
||||
'stream_bandwidth': notify_params['bandwidth'],
|
||||
@@ -800,6 +805,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'artist_name': artist_name,
|
||||
'album_name': album_name,
|
||||
'track_name': track_name,
|
||||
'track_artist': notify_params['original_title'] or notify_params['grandparent_title'],
|
||||
'season_num': season_num,
|
||||
'season_num00': season_num00,
|
||||
'episode_num': episode_num,
|
||||
@@ -1096,7 +1102,10 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
|
||||
|
||||
service = helpers.get_img_service()
|
||||
|
||||
if service == 'cloudinary':
|
||||
if service is None:
|
||||
return img_info
|
||||
|
||||
elif service == 'cloudinary':
|
||||
if fallback == 'cover':
|
||||
w, h = 1000, 1000
|
||||
elif fallback == 'art':
|
||||
@@ -1132,7 +1141,7 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
|
||||
|
||||
elif not database_img_info and img:
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_image(**image_info)
|
||||
result = pms_connect.get_image(refresh=True, **image_info)
|
||||
|
||||
if result and result[0]:
|
||||
img_url = delete_hash = ''
|
||||
|
@@ -420,7 +420,7 @@ def get_notifiers(notifier_id=None, notify_action=None):
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
|
||||
% (', '.join(notify_actions), where), args=args)
|
||||
|
||||
|
||||
for item in result:
|
||||
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
||||
|
||||
@@ -509,7 +509,7 @@ def add_notifier_config(agent_id=None, **kwargs):
|
||||
'agent_name': agent['name'],
|
||||
'agent_label': agent['label'],
|
||||
'friendly_name': '',
|
||||
'notifier_config': json.dumps(get_agent_class(agent_id=agent['id']).config),
|
||||
'notifier_config': json.dumps(agent_class.config),
|
||||
'custom_conditions': json.dumps(DEFAULT_CUSTOM_CONDITIONS),
|
||||
'custom_conditions_logic': ''
|
||||
}
|
||||
@@ -540,7 +540,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
else:
|
||||
logger.error(u"Tautulli Notifiers :: Unable to set exisiting notifier: invalid agent_id %s."
|
||||
logger.error(u"Tautulli Notifiers :: Unable to set existing notifier: invalid agent_id %s."
|
||||
% agent_id)
|
||||
return False
|
||||
|
||||
@@ -570,7 +570,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
||||
'agent_name': agent['name'],
|
||||
'agent_label': agent['label'],
|
||||
'friendly_name': kwargs.get('friendly_name', ''),
|
||||
'notifier_config': json.dumps(notifier_config),
|
||||
'notifier_config': json.dumps(agent_class.config),
|
||||
'custom_conditions': kwargs.get('custom_conditions', json.dumps(DEFAULT_CUSTOM_CONDITIONS)),
|
||||
'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
|
||||
}
|
||||
@@ -661,9 +661,9 @@ class PrettyMetadata(object):
|
||||
poster_url = self.parameters['poster_url']
|
||||
if not poster_url:
|
||||
if self.media_type in ('artist', 'album', 'track'):
|
||||
poster_url = 'http://tautulli.com/images/cover.png'
|
||||
poster_url = common.ONLINE_COVER_THUMB
|
||||
else:
|
||||
poster_url = 'http://tautulli.com/images/poster.png'
|
||||
poster_url = common.ONLINE_POSTER_THUMB
|
||||
return poster_url
|
||||
|
||||
def get_provider_name(self, provider):
|
||||
@@ -728,7 +728,7 @@ class PrettyMetadata(object):
|
||||
elif self.media_type == 'album':
|
||||
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
|
||||
elif self.media_type == 'track':
|
||||
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['track_name'])
|
||||
title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist'])
|
||||
return title.encode("utf-8")
|
||||
|
||||
def get_description(self):
|
||||
@@ -807,7 +807,7 @@ class Notifier(object):
|
||||
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
||||
verify_msg = " Verify you notification agent settings are correct."
|
||||
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed.{}".format(verify_msg, name=self.NAME))
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME))
|
||||
|
||||
if err_msg:
|
||||
logger.error(u"Tautulli Notifiers :: {}".format(err_msg))
|
||||
@@ -1145,7 +1145,8 @@ class DISCORD(Notifier):
|
||||
plex_url = pretty_metadata.get_plex_url()
|
||||
|
||||
# Build Discord post attachment
|
||||
attachment = {'title': title
|
||||
attachment = {'title': title,
|
||||
'timestamp': helpers.utc_now_iso()
|
||||
}
|
||||
|
||||
if self.config['color']:
|
||||
@@ -1295,11 +1296,19 @@ class EMAIL(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['html_support']:
|
||||
plain = MIMEText(None, 'plain', 'utf-8')
|
||||
plain.replace_header('Content-Transfer-Encoding', 'quoted-printable')
|
||||
plain.set_payload(kwargs.get('plaintext', bleach.clean(body, strip=True)), 'utf-8')
|
||||
|
||||
html = MIMEText(body, 'html', 'utf-8')
|
||||
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg.attach(MIMEText(bleach.clean(body, strip=True), 'plain', 'utf-8'))
|
||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
msg.attach(plain)
|
||||
msg.attach(html)
|
||||
else:
|
||||
msg = MIMEText(body, 'plain', 'utf-8')
|
||||
msg = MIMEText(None, 'plain', 'utf-8')
|
||||
msg.replace_header('Content-Transfer-Encoding', 'quoted-printable')
|
||||
msg.set_payload(body, 'utf-8')
|
||||
|
||||
msg['Message-ID'] = email.utils.make_msgid()
|
||||
msg['Date'] = email.utils.formatdate(localtime=True)
|
||||
@@ -1310,26 +1319,32 @@ class EMAIL(Notifier):
|
||||
|
||||
recipients = self.config['to'] + self.config['cc'] + self.config['bcc']
|
||||
|
||||
mailserver = None
|
||||
success = False
|
||||
|
||||
try:
|
||||
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
|
||||
mailserver.ehlo()
|
||||
|
||||
if self.config['tls']:
|
||||
mailserver.starttls()
|
||||
|
||||
mailserver.ehlo()
|
||||
mailserver.ehlo()
|
||||
|
||||
if self.config['smtp_user']:
|
||||
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
|
||||
|
||||
mailserver.sendmail(self.config['from'], recipients, msg.as_string())
|
||||
mailserver.quit()
|
||||
|
||||
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
||||
return True
|
||||
success = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
||||
return False
|
||||
|
||||
finally:
|
||||
if mailserver:
|
||||
mailserver.quit()
|
||||
|
||||
return success
|
||||
|
||||
def get_user_emails(self):
|
||||
emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']}
|
||||
@@ -1452,7 +1467,7 @@ class FACEBOOK(Notifier):
|
||||
|
||||
return facebook.auth_url(app_id=app_id,
|
||||
canvas_url=redirect_uri,
|
||||
perms=['user_managed_groups','publish_actions'])
|
||||
perms=['publish_to_groups'])
|
||||
|
||||
def _get_credentials(self, code=''):
|
||||
logger.info(u"Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))
|
||||
@@ -3464,7 +3479,7 @@ class TWITTER(Notifier):
|
||||
poster_url = parameters.get('poster_url','')
|
||||
|
||||
# Hack to add media type to attachment
|
||||
if poster_url:
|
||||
if poster_url and not helpers.get_img_service():
|
||||
poster_url += '.png'
|
||||
|
||||
if self.config['incl_subject']:
|
||||
|
@@ -49,6 +49,7 @@ def extract_plexivity_xml(xml=None):
|
||||
grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey')
|
||||
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
|
||||
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
|
||||
original_title = helpers.get_xml_attr(a, 'originalTitle')
|
||||
guid = helpers.get_xml_attr(a, 'guid')
|
||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||
media_index = helpers.get_xml_attr(a, 'index')
|
||||
@@ -180,9 +181,10 @@ def extract_plexivity_xml(xml=None):
|
||||
'duration': duration,
|
||||
'grandparent_rating_key': grandparent_rating_key,
|
||||
'grandparent_thumb': grandparent_thumb,
|
||||
'grandparent_title': grandparent_title,
|
||||
'parent_title': parent_title,
|
||||
'title': title,
|
||||
'parent_title': parent_title,
|
||||
'grandparent_title': grandparent_title,
|
||||
'original_title': original_title,
|
||||
'tagline': tagline,
|
||||
'guid': guid,
|
||||
'section_id': section_id,
|
||||
@@ -339,6 +341,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
|
||||
'title': row['title'],
|
||||
'parent_title': extracted_xml['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': extracted_xml['original_title'],
|
||||
'full_title': row['full_title'],
|
||||
'user_id': user_id,
|
||||
'user': row['user'],
|
||||
@@ -380,6 +383,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
|
||||
'title': row['title'],
|
||||
'parent_title': extracted_xml['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': extracted_xml['original_title'],
|
||||
'media_index': extracted_xml['media_index'],
|
||||
'parent_media_index': extracted_xml['parent_media_index'],
|
||||
'thumb': extracted_xml['thumb'],
|
||||
|
@@ -45,6 +45,7 @@ def extract_plexwatch_xml(xml=None):
|
||||
duration = helpers.get_xml_attr(a, 'duration')
|
||||
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
|
||||
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
|
||||
original_title = helpers.get_xml_attr(a, 'originalTitle')
|
||||
guid = helpers.get_xml_attr(a, 'guid')
|
||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||
media_index = helpers.get_xml_attr(a, 'index')
|
||||
@@ -172,9 +173,10 @@ def extract_plexwatch_xml(xml=None):
|
||||
'art': art,
|
||||
'duration': duration,
|
||||
'grandparent_thumb': grandparent_thumb,
|
||||
'grandparent_title': grandparent_title,
|
||||
'parent_title': parent_title,
|
||||
'title': title,
|
||||
'parent_title': parent_title,
|
||||
'grandparent_title': grandparent_title,
|
||||
'original_title': original_title,
|
||||
'tagline': tagline,
|
||||
'guid': guid,
|
||||
'section_id': section_id,
|
||||
@@ -332,6 +334,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
||||
'title': row['title'],
|
||||
'parent_title': extracted_xml['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': extracted_xml['original_title'],
|
||||
'full_title': row['full_title'],
|
||||
'user_id': user_id,
|
||||
'user': row['user'],
|
||||
@@ -373,6 +376,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
||||
'title': row['title'],
|
||||
'parent_title': extracted_xml['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': extracted_xml['original_title'],
|
||||
'media_index': extracted_xml['media_index'],
|
||||
'parent_media_index': extracted_xml['parent_media_index'],
|
||||
'thumb': extracted_xml['thumb'],
|
||||
|
@@ -512,6 +512,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(m, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(m, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
|
||||
@@ -661,6 +662,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -708,6 +710,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -752,6 +755,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -797,6 +801,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -840,6 +845,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -884,6 +890,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -920,6 +927,8 @@ class PmsConnect(object):
|
||||
elif metadata_type == 'track':
|
||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||
album_details = self.get_metadata_details(parent_rating_key)
|
||||
track_artist = helpers.get_xml_attr(metadata_main, 'originalTitle') or \
|
||||
helpers.get_xml_attr(metadata_main, 'grandparentTitle')
|
||||
metadata = {'media_type': metadata_type,
|
||||
'section_id': section_id,
|
||||
'library_name': library_name,
|
||||
@@ -929,6 +938,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -957,8 +967,8 @@ class PmsConnect(object):
|
||||
'genres': album_details['genres'],
|
||||
'labels': album_details['labels'],
|
||||
'collections': album_details['collections'],
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
helpers.get_xml_attr(metadata_main, 'title')),
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'),
|
||||
track_artist),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
}
|
||||
|
||||
@@ -972,6 +982,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -1016,6 +1027,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -1060,6 +1072,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -1105,6 +1118,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -1658,6 +1672,8 @@ class PmsConnect(object):
|
||||
'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'),
|
||||
'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
|
||||
'synced_version': 1 if sync_id else 0,
|
||||
'live': int(helpers.get_xml_attr(session, 'live') == '1'),
|
||||
'live_uuid': helpers.get_xml_attr(stream_media_info, 'uuid'),
|
||||
'indexes': int(indexes == 'sd'),
|
||||
'bif_thumb': bif_thumb,
|
||||
'subtitles': 1 if subtitle_id and subtitle_selected else 0
|
||||
@@ -1670,9 +1686,7 @@ class PmsConnect(object):
|
||||
if not helpers.get_xml_attr(session, 'ratingKey').isdigit():
|
||||
channel_stream = 1
|
||||
|
||||
clip_media = session.getElementsByTagName('Media')[0]
|
||||
clip_part = clip_media.getElementsByTagName('Part')[0]
|
||||
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
|
||||
audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels')
|
||||
metadata_details = {'media_type': media_type,
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'library_name': helpers.get_xml_attr(session, 'librarySectionTitle'),
|
||||
@@ -1682,6 +1696,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(session, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(session, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(session, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
@@ -1710,18 +1725,17 @@ class PmsConnect(object):
|
||||
'genres': [],
|
||||
'labels': [],
|
||||
'full_title': helpers.get_xml_attr(session, 'title'),
|
||||
'container': helpers.get_xml_attr(clip_media, 'container') \
|
||||
or helpers.get_xml_attr(clip_part, 'container'),
|
||||
'height': helpers.get_xml_attr(clip_media, 'height'),
|
||||
'width': helpers.get_xml_attr(clip_media, 'width'),
|
||||
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
|
||||
'video_resolution': helpers.get_xml_attr(clip_media, 'videoResolution'),
|
||||
'audio_codec': helpers.get_xml_attr(clip_media, 'audioCodec'),
|
||||
'container': helpers.get_xml_attr(stream_media_info, 'container') \
|
||||
or helpers.get_xml_attr(stream_media_parts_info, 'container'),
|
||||
'height': helpers.get_xml_attr(stream_media_info, 'height'),
|
||||
'width': helpers.get_xml_attr(stream_media_info, 'width'),
|
||||
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
|
||||
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution'),
|
||||
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
|
||||
'audio_channels': audio_channels,
|
||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
||||
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
|
||||
'live': int(helpers.get_xml_attr(session, 'live') == '1'),
|
||||
'extra_type': helpers.get_xml_attr(session, 'extraType'),
|
||||
'sub_type': helpers.get_xml_attr(session, 'subtype')
|
||||
}
|
||||
@@ -1790,13 +1804,12 @@ class PmsConnect(object):
|
||||
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
||||
|
||||
# Overrides for live sessions
|
||||
if metadata_details.get('live') and transcode_session:
|
||||
if stream_details['live'] and transcode_session:
|
||||
stream_details['stream_container_decision'] = 'transcode'
|
||||
stream_details['stream_container'] = transcode_details['transcode_container']
|
||||
|
||||
video_details['stream_video_decision'] = transcode_details['video_decision']
|
||||
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
|
||||
stream_details['stream_video_resolution'] = metadata_details['video_resolution']
|
||||
|
||||
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
|
||||
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
|
||||
@@ -1994,6 +2007,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(m, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(m, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
|
||||
@@ -2311,6 +2325,7 @@ class PmsConnect(object):
|
||||
'title': helpers.get_xml_attr(item, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
|
||||
'original_title': helpers.get_xml_attr(item, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(item, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
|
||||
@@ -2436,7 +2451,7 @@ class PmsConnect(object):
|
||||
return labels_list
|
||||
|
||||
def get_image(self, img=None, width=1000, height=1500, opacity=None, background=None, blur=None,
|
||||
img_format='png', clip=False, **kwargs):
|
||||
img_format='png', clip=False, refresh=False, **kwargs):
|
||||
"""
|
||||
Return image data as array.
|
||||
Array contains the image content type and image binary
|
||||
@@ -2454,6 +2469,9 @@ class PmsConnect(object):
|
||||
height = height or 1500
|
||||
|
||||
if img:
|
||||
if refresh:
|
||||
img = '{}/{}'.format(img.rstrip('/'), int(time.time()))
|
||||
|
||||
if clip:
|
||||
params = {'url': '%s&%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
|
||||
else:
|
||||
@@ -2544,7 +2562,7 @@ class PmsConnect(object):
|
||||
metadata = self.get_metadata_details(rating_key=rating_key)
|
||||
search_results_list[metadata['media_type']].append(metadata)
|
||||
|
||||
output = {'results_count': sum(len(s) for s in search_results_list.items()),
|
||||
output = {'results_count': sum(len(s) for s in search_results_list.values()),
|
||||
'results_list': search_results_list
|
||||
}
|
||||
|
||||
|
@@ -201,9 +201,10 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
||||
'grandparent_thumb': common.DEFAULT_POSTER_THUMB,
|
||||
'thumb': common.DEFAULT_POSTER_THUMB,
|
||||
'bif_thumb': '',
|
||||
'grandparent_title': 'Plex Media',
|
||||
'parent_title': 'Plex Media',
|
||||
'title': 'Plex Media',
|
||||
'parent_title': 'Plex Media',
|
||||
'grandparent_title': 'Plex Media',
|
||||
'original_title': 'Plex Media',
|
||||
'rating_key': '',
|
||||
'parent_rating_key': '',
|
||||
'grandparent_rating_key': '',
|
||||
|
@@ -521,7 +521,8 @@ class Users(object):
|
||||
if str(user_id).isdigit():
|
||||
query = 'SELECT session_history.id, session_history.media_type, ' \
|
||||
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
|
||||
'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
|
||||
'title, parent_title, grandparent_title, original_title, ' \
|
||||
'thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
|
||||
'year, started, user ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
@@ -552,6 +553,7 @@ class Users(object):
|
||||
'title': row['title'],
|
||||
'parent_title': row['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': row['original_title'],
|
||||
'thumb': thumb,
|
||||
'media_index': row['media_index'],
|
||||
'parent_media_index': row['parent_media_index'],
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.2-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.10-beta"
|
||||
|
@@ -56,7 +56,7 @@ import web_socket
|
||||
from plexpy.api2 import API2
|
||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json
|
||||
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
|
||||
from plexpy.webauth import AuthController, requireAuth, member_of, name_is
|
||||
from plexpy.webauth import AuthController, requireAuth, member_of
|
||||
|
||||
|
||||
def serve_template(templatename, **kwargs):
|
||||
@@ -1614,6 +1614,7 @@ class WebInterface(object):
|
||||
"full_title": "Game of Thrones - The Red Woman",
|
||||
"grandparent_rating_key": 351,
|
||||
"grandparent_title": "Game of Thrones",
|
||||
"original_title": "",
|
||||
"group_count": 1,
|
||||
"group_ids": "1124",
|
||||
"id": 1124,
|
||||
@@ -1715,6 +1716,77 @@ class WebInterface(object):
|
||||
|
||||
return serve_template(templatename="stream_data.html", title="Stream Data", data=stream_data, user=user)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@addtoapi('get_stream_data')
|
||||
def get_stream_data_api(self, row_id=None, session_key=None, **kwargs):
|
||||
""" Get the stream details from history or current stream.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
row_id (int): The row ID number for the history item, OR
|
||||
session_key (int): The session key of the current stream
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"aspect_ratio": "2.35",
|
||||
"audio_bitrate": 231,
|
||||
"audio_channels": 6,
|
||||
"audio_codec": "aac",
|
||||
"audio_decision": "transcode",
|
||||
"bitrate": 2731,
|
||||
"container": "mp4",
|
||||
"current_session": "",
|
||||
"grandparent_title": "",
|
||||
"media_type": "movie",
|
||||
"optimized_version": "",
|
||||
"optimized_version_profile": "",
|
||||
"optimized_version_title": "",
|
||||
"original_title": "",
|
||||
"pre_tautulli": "",
|
||||
"quality_profile": "1.5 Mbps 480p",
|
||||
"stream_audio_bitrate": 203,
|
||||
"stream_audio_channels": 2,
|
||||
"stream_audio_codec": "aac",
|
||||
"stream_audio_decision": "transcode",
|
||||
"stream_bitrate": 730,
|
||||
"stream_container": "mkv",
|
||||
"stream_container_decision": "transcode",
|
||||
"stream_subtitle_codec": "",
|
||||
"stream_subtitle_decision": "",
|
||||
"stream_video_bitrate": 527,
|
||||
"stream_video_codec": "h264",
|
||||
"stream_video_decision": "transcode",
|
||||
"stream_video_framerate": "24p",
|
||||
"stream_video_height": 306,
|
||||
"stream_video_resolution": "SD",
|
||||
"stream_video_width": 720,
|
||||
"subtitle_codec": "",
|
||||
"subtitles": "",
|
||||
"synced_version": "",
|
||||
"synced_version_profile": "",
|
||||
"title": "Frozen",
|
||||
"transcode_hw_decoding": "",
|
||||
"transcode_hw_encoding": "",
|
||||
"video_bitrate": 2500,
|
||||
"video_codec": "h264",
|
||||
"video_decision": "transcode",
|
||||
"video_framerate": "24p",
|
||||
"video_height": 816,
|
||||
"video_resolution": "1080",
|
||||
"video_width": 1920
|
||||
}
|
||||
```
|
||||
"""
|
||||
data_factory = datafactory.DataFactory()
|
||||
stream_data = data_factory.get_stream_details(row_id, session_key)
|
||||
|
||||
return stream_data
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
def get_ip_address_details(self, ip_address=None, **kwargs):
|
||||
@@ -2755,7 +2827,11 @@ class WebInterface(object):
|
||||
"tvmaze_lookup": checked(plexpy.CONFIG.TVMAZE_LOOKUP),
|
||||
"show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS,
|
||||
"newsletter_dir": plexpy.CONFIG.NEWSLETTER_DIR,
|
||||
"newsletter_self_hosted": checked(plexpy.CONFIG.NEWSLETTER_SELF_HOSTED)
|
||||
"newsletter_self_hosted": checked(plexpy.CONFIG.NEWSLETTER_SELF_HOSTED),
|
||||
"newsletter_auth": plexpy.CONFIG.NEWSLETTER_AUTH,
|
||||
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
|
||||
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
|
||||
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
||||
}
|
||||
|
||||
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
|
||||
@@ -2777,7 +2853,7 @@ class WebInterface(object):
|
||||
"allow_guest_access", "cache_images", "http_proxy", "http_basic_auth", "notify_concurrent_by_ip",
|
||||
"history_table_activity", "plexpy_auto_update",
|
||||
"themoviedb_lookup", "tvmaze_lookup", "http_plex_admin",
|
||||
"newsletter_self_hosted"
|
||||
"newsletter_self_hosted", "newsletter_inline_styles"
|
||||
]
|
||||
for checked_config in checked_configs:
|
||||
if checked_config not in kwargs:
|
||||
@@ -3148,7 +3224,7 @@ class WebInterface(object):
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def set_notifier_config(self, notifier_id=None, agent_id=None, **kwargs):
|
||||
""" Configure an exisitng notificaiton agent.
|
||||
""" Configure an existing notification agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
@@ -3267,10 +3343,10 @@ class WebInterface(object):
|
||||
return {'result': 'success', 'message': 'Notification queued.'}
|
||||
else:
|
||||
logger.debug(u"Unable to send %snotification, invalid notifier_id %s." % (test, notifier_id))
|
||||
return {'result': 'success', 'message': 'Invalid notifier id %s.' % notifier_id}
|
||||
return {'result': 'error', 'message': 'Invalid notifier id %s.' % notifier_id}
|
||||
else:
|
||||
logger.debug(u"Unable to send %snotification, no notifier_id received." % test)
|
||||
return {'result': 'success', 'message': 'No notifier id received.'}
|
||||
return {'result': 'error', 'message': 'No notifier id received.'}
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@@ -3407,7 +3483,7 @@ class WebInterface(object):
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def set_mobile_device_config(self, mobile_device_id=None, **kwargs):
|
||||
""" Configure an exisitng notificaiton agent.
|
||||
""" Configure an existing notification agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
@@ -3962,13 +4038,20 @@ class WebInterface(object):
|
||||
return
|
||||
|
||||
if rating_key and not img:
|
||||
img = '/library/metadata/%s/thumb/1337' % rating_key
|
||||
if fallback == 'art':
|
||||
img = '/library/metadata/{}/art'.format(rating_key)
|
||||
else:
|
||||
img = '/library/metadata/{}/thumb'.format(rating_key)
|
||||
|
||||
img_string = img.rsplit('/', 1)[0] if '/library/metadata' in img else img
|
||||
img_string = '{}{}{}{}{}{}'.format(img_string, width, height, opacity, background, blur)
|
||||
img_split = img.split('/')
|
||||
img = '/'.join(img_split[:5])
|
||||
rating_key = rating_key or img_split[3]
|
||||
|
||||
fp = hashlib.md5(img_string).hexdigest()
|
||||
fp += '.%s' % img_format # we want to be able to preview the thumbs
|
||||
img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
|
||||
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
|
||||
img_hash = hashlib.sha256(img_string).hexdigest()
|
||||
|
||||
fp = '{}.{}'.format(img_hash, img_format) # we want to be able to preview the thumbs
|
||||
c_dir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images')
|
||||
ffp = os.path.join(c_dir, fp)
|
||||
|
||||
@@ -3994,7 +4077,8 @@ class WebInterface(object):
|
||||
background=background,
|
||||
blur=blur,
|
||||
img_format=img_format,
|
||||
clip=clip)
|
||||
clip=clip,
|
||||
refresh=refresh)
|
||||
|
||||
if result and result[0]:
|
||||
cherrypy.response.headers['Content-type'] = result[1]
|
||||
@@ -4023,6 +4107,15 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def image(self, *args, **kwargs):
|
||||
if args:
|
||||
cherrypy.response.headers['Cache-Control'] = 'max-age=3600' # 1 hour
|
||||
|
||||
if len(args) >= 2 and args[0] == 'images':
|
||||
resource_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/default/')
|
||||
try:
|
||||
return serve_file(path=os.path.join(resource_dir, *args), content_type='image/png')
|
||||
except NotFound:
|
||||
return
|
||||
|
||||
img_hash = args[0].split('.')[0]
|
||||
|
||||
if img_hash in ('poster', 'cover', 'art'):
|
||||
@@ -4040,7 +4133,7 @@ class WebInterface(object):
|
||||
|
||||
if img_info:
|
||||
kwargs.update(img_info)
|
||||
return self.real_pms_image_proxy(**kwargs)
|
||||
return self.real_pms_image_proxy(refresh=True, **kwargs)
|
||||
|
||||
return
|
||||
|
||||
@@ -4161,16 +4254,18 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def delete_hosted_images(self, rating_key='', service='', **kwargs):
|
||||
def delete_hosted_images(self, rating_key='', service='', delete_all=False, **kwargs):
|
||||
""" Delete the images uploaded to image hosting services.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
rating_key (int): 1234
|
||||
(Note: Must be the movie, show, season, artist, or album rating key)
|
||||
Optional parameters:
|
||||
service (str): imgur or cloudinary
|
||||
(Note: Defaults to service in Image Hosting setting)
|
||||
service (str): 'imgur' or 'cloudinary'
|
||||
delete_all (bool): 'true' to delete all images form the service
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -4179,8 +4274,10 @@ class WebInterface(object):
|
||||
```
|
||||
"""
|
||||
|
||||
delete_all = (delete_all == 'true')
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.delete_img_info(rating_key=rating_key, service=service)
|
||||
result = data_factory.delete_img_info(rating_key=rating_key, service=service, delete_all=delete_all)
|
||||
|
||||
if result:
|
||||
return {'result': 'success', 'message': 'Deleted hosted images from %s.' % result.capitalize()}
|
||||
@@ -4531,6 +4628,7 @@ class WebInterface(object):
|
||||
}
|
||||
],
|
||||
"media_type": "episode",
|
||||
"original_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
@@ -4589,6 +4687,7 @@ class WebInterface(object):
|
||||
"library_name": "",
|
||||
"media_index": "1",
|
||||
"media_type": "episode",
|
||||
"original_title": "",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
||||
@@ -4860,6 +4959,7 @@ class WebInterface(object):
|
||||
"optimized_version_profile": "",
|
||||
"optimized_version_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"original_title": "",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
||||
@@ -5459,7 +5559,7 @@ class WebInterface(object):
|
||||
Returns:
|
||||
json:
|
||||
[{"id": 1,
|
||||
"agent_id": 13,
|
||||
"agent_id": 0,
|
||||
"agent_name": "recently_added",
|
||||
"agent_label": "Recently Added",
|
||||
"friendly_name": "",
|
||||
@@ -5519,15 +5619,24 @@ class WebInterface(object):
|
||||
Returns:
|
||||
json:
|
||||
{"id": 1,
|
||||
"agent_id": 13,
|
||||
"agent_id": 0,
|
||||
"agent_name": "recently_added",
|
||||
"agent_label": "Recently Added",
|
||||
"friendly_name": "",
|
||||
"id_name": "",
|
||||
"cron": "0 0 * * 1",
|
||||
"active": 1
|
||||
"config": {"time_frame": 7,
|
||||
"time_frame_units": "days",
|
||||
"incl_libraries": [1, 2]
|
||||
"active": 1,
|
||||
"subject": "Recently Added to {server_name}! ({end_date})",
|
||||
"body": "View the newsletter here: {newsletter_url}",
|
||||
"message": "",
|
||||
"config": {"custom_cron": 0,
|
||||
"filename": "newsletter_{newsletter_uuid}.html",
|
||||
"formatted": 1,
|
||||
"incl_libraries": ["1", "2"],
|
||||
"notifier_id": 1,
|
||||
"save_only": 0,
|
||||
"time_frame": 7,
|
||||
"time_frame_units": "days"
|
||||
},
|
||||
"email_config": {...},
|
||||
"config_options": [{...}, ...],
|
||||
@@ -5574,19 +5683,15 @@ class WebInterface(object):
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
|
||||
""" Configure an exisitng notificaiton agent.
|
||||
""" Configure an existing newsletter agent.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
newsletter_id (int): The newsletter config to update
|
||||
agent_id (int): The newsletter type of the newsletter
|
||||
newsletter_id (int): The newsletter config to update
|
||||
agent_id (int): The newsletter type of the newsletter
|
||||
|
||||
Optional parameters:
|
||||
Pass all the config options for the agent with the agent prefix:
|
||||
e.g. For Recently Added: recently_added_last_days
|
||||
recently_added_incl_movies
|
||||
recently_added_incl_shows
|
||||
recently_added_incl_artists
|
||||
Pass all the config options for the agent with the 'newsletter_config_' and 'newsletter_email_' prefix.
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -5604,7 +5709,6 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi("notify_newsletter")
|
||||
def send_newsletter(self, newsletter_id=None, subject='', body='', message='', notify_action='', **kwargs):
|
||||
""" Send a newsletter using Tautulli.
|
||||
|
||||
@@ -5644,7 +5748,29 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def newsletter(self, *args, **kwargs):
|
||||
request_uri = cherrypy.request.wsgi_environ['REQUEST_URI']
|
||||
if plexpy.CONFIG.NEWSLETTER_AUTH == 2:
|
||||
redirect_uri = request_uri.replace('/newsletter', '/newsletter_auth')
|
||||
raise cherrypy.HTTPRedirect(redirect_uri)
|
||||
|
||||
elif plexpy.CONFIG.NEWSLETTER_AUTH == 1 and plexpy.CONFIG.NEWSLETTER_PASSWORD:
|
||||
if len(args) >= 2 and args[0] == 'image':
|
||||
return self.newsletter_auth(*args, **kwargs)
|
||||
elif kwargs.pop('key', None) == plexpy.CONFIG.NEWSLETTER_PASSWORD:
|
||||
return self.newsletter_auth(*args, **kwargs)
|
||||
else:
|
||||
return serve_template(templatename="newsletter_auth.html",
|
||||
title="Newsletter Login",
|
||||
uri=request_uri)
|
||||
|
||||
else:
|
||||
return self.newsletter_auth(*args, **kwargs)
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
def newsletter_auth(self, *args, **kwargs):
|
||||
if args:
|
||||
# Keep this for backwards compatibility for images through /newsletter/image
|
||||
if len(args) >= 2 and args[0] == 'image':
|
||||
if args[1] == 'images':
|
||||
resource_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/default/')
|
||||
@@ -5653,10 +5779,17 @@ class WebInterface(object):
|
||||
except NotFound:
|
||||
return
|
||||
|
||||
return self.image(args[1], refresh=True)
|
||||
return self.image(args[1])
|
||||
|
||||
newsletter_uuid = args[0]
|
||||
newsletter = newsletter_handler.get_newsletter(newsletter_uuid=newsletter_uuid)
|
||||
if len(args) >= 2 and args[0] == 'id':
|
||||
newsletter_id_name = args[1]
|
||||
newsletter_uuid = None
|
||||
else:
|
||||
newsletter_id_name = None
|
||||
newsletter_uuid = args[0]
|
||||
|
||||
newsletter = newsletter_handler.get_newsletter(newsletter_uuid=newsletter_uuid,
|
||||
newsletter_id_name=newsletter_id_name)
|
||||
return newsletter
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -5670,12 +5803,14 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@requireAuth(member_of("admin"))
|
||||
def real_newsletter(self, newsletter_id=None, start_date=None, end_date=None,
|
||||
preview=False, master=False, raw=False, **kwargs):
|
||||
preview=False, raw=False, **kwargs):
|
||||
if newsletter_id and newsletter_id != 'None':
|
||||
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||
|
||||
if newsletter:
|
||||
newsletter_agent = newsletters.get_agent_class(agent_id=newsletter['agent_id'],
|
||||
newsletter_agent = newsletters.get_agent_class(newsletter_id=newsletter_id,
|
||||
newsletter_id_name=newsletter['id_name'],
|
||||
agent_id=newsletter['agent_id'],
|
||||
config=newsletter['config'],
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
@@ -5683,14 +5818,13 @@ class WebInterface(object):
|
||||
body=newsletter['body'],
|
||||
message=newsletter['message'])
|
||||
preview = (preview == 'true')
|
||||
master = (master == 'true')
|
||||
raw = (raw == 'true')
|
||||
|
||||
if raw:
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||
return json.dumps(newsletter_agent.raw_data(preview=preview))
|
||||
|
||||
return newsletter_agent.generate_newsletter(preview=preview, master=master)
|
||||
return newsletter_agent.generate_newsletter(preview=preview)
|
||||
|
||||
logger.error(u"Failed to retrieve newsletter: Invalid newsletter_id %s" % newsletter_id)
|
||||
return "Failed to retrieve newsletter: invalid newsletter_id parameter"
|
||||
|
Reference in New Issue
Block a user