Compare commits
81 Commits
v2.1.6-bet
...
v2.1.12
Author | SHA1 | Date | |
---|---|---|---|
![]() |
331be52327 | ||
![]() |
6ece690e23 | ||
![]() |
6b63a1399e | ||
![]() |
5c77cf652b | ||
![]() |
d6f9a82edb | ||
![]() |
a887489666 | ||
![]() |
8c0bcd0059 | ||
![]() |
c18ee81130 | ||
![]() |
44428cc6e5 | ||
![]() |
c19cc858bd | ||
![]() |
668913fd60 | ||
![]() |
50c5407a46 | ||
![]() |
939755d3b7 | ||
![]() |
54f4696713 | ||
![]() |
c85af521fe | ||
![]() |
917d19db85 | ||
![]() |
7292f25eb9 | ||
![]() |
22a2ad4bc7 | ||
![]() |
95e56f5ea5 | ||
![]() |
ed24232a0a | ||
![]() |
15225faee7 | ||
![]() |
041a35a35a | ||
![]() |
6d365c174a | ||
![]() |
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 |
308
API.md
308
API.md
@@ -32,6 +32,21 @@ General optional parameters:
|
|||||||
|
|
||||||
## API methods
|
## 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_notifier_config
|
||||||
Add a new notification agent.
|
Add a new notification agent.
|
||||||
|
|
||||||
@@ -93,27 +108,30 @@ Returns:
|
|||||||
Delete and recreate the cache directory.
|
Delete and recreate the cache directory.
|
||||||
|
|
||||||
|
|
||||||
### delete_image_cache
|
### delete_hosted_images
|
||||||
Delete and recreate the image cache directory.
|
Delete the images uploaded to image hosting services.
|
||||||
|
|
||||||
|
|
||||||
### delete_imgur_poster
|
|
||||||
Delete the Imgur poster.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
rating_key (int): 1234
|
rating_key (int): 1234
|
||||||
(Note: Must be the movie, show, season, artist, or album rating key)
|
(Note: Must be the movie, show, season, artist, or album rating key)
|
||||||
Optional parameters:
|
service (str): 'imgur' or 'cloudinary'
|
||||||
None
|
delete_all (bool): 'true' to delete all images form the service
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
{"result": "success",
|
{"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_library
|
||||||
Delete a library section from Tautulli. Also erases all history for the 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_notification_log
|
||||||
Delete the Tautulli notification logs.
|
Delete the Tautulli notification logs.
|
||||||
|
|
||||||
@@ -386,6 +434,7 @@ Returns:
|
|||||||
"optimized_version_profile": "",
|
"optimized_version_profile": "",
|
||||||
"optimized_version_title": "",
|
"optimized_version_title": "",
|
||||||
"originally_available_at": "2016-04-24",
|
"originally_available_at": "2016-04-24",
|
||||||
|
"original_title": "",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
"parent_rating_key": "153036",
|
"parent_rating_key": "153036",
|
||||||
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
||||||
@@ -630,6 +679,7 @@ Returns:
|
|||||||
"full_title": "Game of Thrones - The Red Woman",
|
"full_title": "Game of Thrones - The Red Woman",
|
||||||
"grandparent_rating_key": 351,
|
"grandparent_rating_key": 351,
|
||||||
"grandparent_title": "Game of Thrones",
|
"grandparent_title": "Game of Thrones",
|
||||||
|
"original_title": "",
|
||||||
"group_count": 1,
|
"group_count": 1,
|
||||||
"group_ids": "1124",
|
"group_ids": "1124",
|
||||||
"id": 1124,
|
"id": 1124,
|
||||||
@@ -916,9 +966,9 @@ Optional parameters:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
[{"section_id": 1, "section_name": "Movies"},
|
[{"section_id": 1, "section_name": "Movies", "section_type": "movie"},
|
||||||
{"section_id": 7, "section_name": "Music"},
|
{"section_id": 7, "section_name": "Music", "section_type": "artist"},
|
||||||
{"section_id": 2, "section_name": "TV Shows"},
|
{"section_id": 2, "section_name": "TV Shows", "section_type": "show"},
|
||||||
{...}
|
{...}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -1124,6 +1174,7 @@ Returns:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"media_type": "episode",
|
"media_type": "episode",
|
||||||
|
"original_title": "",
|
||||||
"originally_available_at": "2016-04-24",
|
"originally_available_at": "2016-04-24",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
"parent_rating_key": "153036",
|
"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_notification_log
|
||||||
Get the data on the Tautulli notification logs table.
|
Get the data on the Tautulli notification logs table.
|
||||||
|
|
||||||
@@ -1174,8 +1328,8 @@ Required parameters:
|
|||||||
None
|
None
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
order_column (str): "timestamp", "agent_name", "notify_action",
|
order_column (str): "timestamp", "notifier_id", "agent_name", "notify_action",
|
||||||
"subject_text", "body_text", "script_args"
|
"subject_text", "body_text",
|
||||||
order_dir (str): "desc" or "asc"
|
order_dir (str): "desc" or "asc"
|
||||||
start (int): Row to start from, 0
|
start (int): Row to start from, 0
|
||||||
length (int): Number of items to return, 25
|
length (int): Number of items to return, 25
|
||||||
@@ -1188,15 +1342,14 @@ Returns:
|
|||||||
"recordsFiltered": 163,
|
"recordsFiltered": 163,
|
||||||
"data":
|
"data":
|
||||||
[{"agent_id": 13,
|
[{"agent_id": 13,
|
||||||
"agent_name": "Telegram",
|
"agent_name": "telegram",
|
||||||
"body_text": "Game of Thrones - S06E01 - The Red Woman [Transcode].",
|
"body_text": "DanyKhaleesi69 started playing The Red Woman.",
|
||||||
"id": 1000,
|
"id": 1000,
|
||||||
"notify_action": "play",
|
"notify_action": "on_play",
|
||||||
"poster_url": "http://i.imgur.com/ZSqS8Ri.jpg",
|
|
||||||
"rating_key": 153037,
|
"rating_key": 153037,
|
||||||
"script_args": "[]",
|
|
||||||
"session_key": 147,
|
"session_key": 147,
|
||||||
"subject_text": "Tautulli (Winterfell-Server)",
|
"subject_text": "Tautulli (Winterfell-Server)",
|
||||||
|
"success": 1,
|
||||||
"timestamp": 1462253821,
|
"timestamp": 1462253821,
|
||||||
"user": "DanyKhaleesi69",
|
"user": "DanyKhaleesi69",
|
||||||
"user_id": 8008135
|
"user_id": 8008135
|
||||||
@@ -1629,6 +1782,7 @@ Returns:
|
|||||||
"library_name": "",
|
"library_name": "",
|
||||||
"media_index": "1",
|
"media_index": "1",
|
||||||
"media_type": "episode",
|
"media_type": "episode",
|
||||||
|
"original_title": "",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
"parent_rating_key": "153036",
|
"parent_rating_key": "153036",
|
||||||
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
"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_stream_type_by_top_10_platforms
|
||||||
Get graph data by 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
|
### notify_recently_added
|
||||||
Send a recently added notification using Tautulli.
|
Send a recently added notification using Tautulli.
|
||||||
|
|
||||||
@@ -2244,8 +2478,12 @@ Required parameters:
|
|||||||
rating_key (str): 54321
|
rating_key (str): 54321
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
width (str): 150
|
width (str): 300
|
||||||
height (str): 255
|
height (str): 450
|
||||||
|
opacity (str): 25
|
||||||
|
background (str): 282828
|
||||||
|
blur (str): 3
|
||||||
|
img_format (str): png
|
||||||
fallback (str): "poster", "cover", "art"
|
fallback (str): "poster", "cover", "art"
|
||||||
refresh (bool): True or False whether to refresh the image cache
|
refresh (bool): True or False whether to refresh the image cache
|
||||||
|
|
||||||
@@ -2312,7 +2550,7 @@ Returns:
|
|||||||
|
|
||||||
|
|
||||||
### set_mobile_device_config
|
### set_mobile_device_config
|
||||||
Configure an exisitng notificaiton agent.
|
Configure an existing notification agent.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
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
|
### set_notifier_config
|
||||||
Configure an exisitng notificaiton agent.
|
Configure an existing notification agent.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
@@ -2370,15 +2624,15 @@ Returns:
|
|||||||
|
|
||||||
|
|
||||||
### terminate_session
|
### terminate_session
|
||||||
Add a new notification agent.
|
Stop a streaming session.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
session_id (str): The id of the session to terminate
|
session_key (int): The session key of the session to terminate, OR
|
||||||
message (str): A custom message to send to the client
|
session_id (str): The session id of the session to terminate
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
None
|
message (str): A custom message to send to the client
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
|
68
CHANGELOG.md
68
CHANGELOG.md
@@ -1,5 +1,73 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.1.12 (2018-06-08)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Change: Blank notification link source means disabled instead of default.
|
||||||
|
* Newsletters:
|
||||||
|
* New: Make collection tags available in the raw newsletter data for custom templates.
|
||||||
|
* API:
|
||||||
|
* New: Ability to terminate a stream using the session key.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.1.11-beta (2018-06-02)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Activity progress bar not updating in some cases.
|
||||||
|
* Fix: Monitory Remote Access setting disabled due to Plex Media Server API changes.
|
||||||
|
* Change: Improved logic for grouping history items without being successive plays.
|
||||||
|
* Notifications:
|
||||||
|
* New: Added filename to notification parameters.
|
||||||
|
* Other:
|
||||||
|
* Fix: Update metadata failing for tracks without track numbers.
|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
## v2.1.6-beta (2018-05-09)
|
||||||
|
|
||||||
* Newsletters:
|
* Newsletters:
|
||||||
|
@@ -4,12 +4,12 @@
|
|||||||
If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request.
|
If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request.
|
||||||
|
|
||||||
### Branches
|
### 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
|
### Python Code
|
||||||
|
|
||||||
#### Compatibility
|
#### 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.
|
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
|
#### Documentation
|
||||||
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
|
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
|
### HTML/Template code
|
||||||
|
|
||||||
#### Compatibility
|
#### Compatibility
|
||||||
HTML5 compatible browsers are targetted. There is no specific mobile version of Tautulli yet.
|
HTML5 compatible browsers are targeted.
|
||||||
|
|
||||||
#### Conventions
|
#### Conventions
|
||||||
* 4 space indentation
|
* 4 space indentation
|
||||||
|
@@ -27,9 +27,9 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
|||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
* [Full preview gallery available on our website](http://tautulli.com)
|
* [Full preview gallery available on our website](https://tautulli.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation and Support
|
## Installation and Support
|
||||||
|
|
||||||
|
@@ -292,6 +292,7 @@ ${next.modalIncludes()}
|
|||||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||||
|
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||||
% endif
|
% endif
|
||||||
|
@@ -69,7 +69,7 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
<td>Platform:</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Python Version:</td>
|
<td>Python Version:</td>
|
||||||
@@ -78,7 +78,7 @@ DOCUMENTATION :: END
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="top-line">Resources:</td>
|
<td class="top-line">Resources:</td>
|
||||||
<td class="top-line">
|
<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" 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 guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="issue">GitHub Issues</a> |
|
||||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Wiki</a> |
|
<a class="no-highlight" 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;
|
background-color: #555;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
|
height: 32px !important;
|
||||||
}
|
}
|
||||||
.react-selectize.root-node .react-selectize-control,
|
.react-selectize.root-node .react-selectize-control,
|
||||||
.selectize-control.form-control .selectize-input {
|
.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 > a.toggle-left,
|
||||||
.stacked-configs > li > span > span.toggle-left {
|
.stacked-configs > li > span > span.toggle-left {
|
||||||
|
float: left;
|
||||||
color: #444;
|
color: #444;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
@@ -2944,16 +2946,6 @@ a .home-platforms-list-cover-face:hover
|
|||||||
.stacked-configs > li > span > span.active {
|
.stacked-configs > li > span > span.active {
|
||||||
color: #f9be03;
|
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 > a.toggle-left,
|
||||||
.stacked-configs > li.mobile-device > span > span.toggle-left {
|
.stacked-configs > li.mobile-device > span > span.toggle-left {
|
||||||
color: #999;
|
color: #999;
|
||||||
@@ -3524,8 +3516,7 @@ a.no-highlight:hover {
|
|||||||
}
|
}
|
||||||
.login-logo {
|
.login-logo {
|
||||||
margin: 0 auto 50px auto;
|
margin: 0 auto 50px auto;
|
||||||
width: 340px;
|
text-align: center;
|
||||||
height: 100px;
|
|
||||||
}
|
}
|
||||||
.login-container .form-group {
|
.login-container .form-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@@ -4097,4 +4088,11 @@ a[data-tab-destination] {
|
|||||||
margin-top: 10px !important;
|
margin-top: 10px !important;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border-top: 1px solid #444;
|
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="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||||
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'track':
|
% 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':
|
% elif data['media_type'] == 'photo':
|
||||||
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
||||||
% elif data['media_type'] == 'clip':
|
% elif data['media_type'] == 'clip':
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="padded-header" id="current-activity-header">
|
<div class="padded-header" id="current-activity-header">
|
||||||
<h3><span id="sessions-shortcut">Activity</span>
|
<h3><span id="sessions-xml">Activity</span>
|
||||||
<small>
|
<small>
|
||||||
<span id="currentActivityHeader" style="display: none;">
|
<span id="currentActivityHeader" style="display: none;">
|
||||||
Streams: <span id="currentActivityHeader-streams"></span> |
|
Streams: <span id="currentActivityHeader-streams"></span> |
|
||||||
@@ -236,7 +236,6 @@
|
|||||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
<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.scrollbar.min.js"></script>
|
||||||
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
||||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
var date_format = 'YYYY-MM-DD';
|
var date_format = 'YYYY-MM-DD';
|
||||||
var time_format = 'hh:mm a';
|
var time_format = 'hh:mm a';
|
||||||
@@ -390,8 +389,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)');
|
$('#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)
|
$('#metadata-grandparent_title-' + key)
|
||||||
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
||||||
.attr('title', s.grandparent_title)
|
.attr('title', s.original_title || s.grandparent_title)
|
||||||
.text(s.grandparent_title);
|
.text(s.original_title || s.grandparent_title);
|
||||||
}
|
}
|
||||||
// Update cover if album changed
|
// Update cover if album changed
|
||||||
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
|
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
|
||||||
@@ -406,7 +405,11 @@
|
|||||||
.text(s.parent_title);
|
.text(s.parent_title);
|
||||||
}
|
}
|
||||||
// Update cover if track changed
|
// 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)
|
$('#metadata-title-' + key)
|
||||||
.attr('href', 'info?rating_key=' + s.rating_key)
|
.attr('href', 'info?rating_key=' + s.rating_key)
|
||||||
.attr('title', s.title)
|
.attr('title', s.title)
|
||||||
@@ -542,7 +545,7 @@
|
|||||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||||
var progress_bar = $('#progress-bar-' + key);
|
var progress_bar = $('#progress-bar-' + key);
|
||||||
progress_bar.data('state', s.state);
|
progress_bar.data('state', s.state);
|
||||||
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
|
if (progress_bar.data('last_view_offset') !== s.view_offset) {
|
||||||
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,6 +675,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'terminate_session',
|
url: 'terminate_session',
|
||||||
data: {
|
data: {
|
||||||
|
session_key: key,
|
||||||
session_id: session_id,
|
session_id: session_id,
|
||||||
message: message
|
message: message
|
||||||
},
|
},
|
||||||
@@ -690,10 +694,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#sessions-shortcut').on('tripleclick', function () {
|
$('#sessions-xml').on('tripleclick', function () {
|
||||||
$.getJSON('return_sessions_url', function(sessions_url) {
|
openPlexXML('/status/sessions');
|
||||||
window.open(sessions_url, '_blank');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
% endif
|
% endif
|
||||||
</script>
|
</script>
|
||||||
|
@@ -83,31 +83,31 @@ DOCUMENTATION :: END
|
|||||||
<ul class="list-unstyled breadcrumb">
|
<ul class="list-unstyled breadcrumb">
|
||||||
% if data['media_type'] in ('movie', 'collection'):
|
% if data['media_type'] in ('movie', 'collection'):
|
||||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li class="active">${data['title']}</li>
|
<li class="active metadata-xml">${data['title']}</li>
|
||||||
% elif data['media_type'] == 'show':
|
% elif data['media_type'] == 'show':
|
||||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li class="active">${data['title']}</li>
|
<li class="active metadata-xml">${data['title']}</li>
|
||||||
% elif data['media_type'] == 'season':
|
% elif data['media_type'] == 'season':
|
||||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||||
<li class="active">Season ${data['media_index']}</li>
|
<li class="active metadata-xml">Season ${data['media_index']}</li>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
||||||
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
|
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
|
||||||
<li class="active">Episode ${data['media_index']} - ${data['title']}</li>
|
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
|
||||||
% elif data['media_type'] == 'artist':
|
% elif data['media_type'] == 'artist':
|
||||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li class="active">${data['title']}</li>
|
<li class="active metadata-xml">${data['title']}</li>
|
||||||
% elif data['media_type'] == 'album':
|
% elif data['media_type'] == 'album':
|
||||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||||
<li class="active">${data['title']}</li>
|
<li class="active metadata-xml">${data['title']}</li>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||||
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
||||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||||
<li class="active">Track ${data['media_index']} - ${data['title']}</li>
|
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
|
||||||
% endif
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,7 +165,7 @@ DOCUMENTATION :: END
|
|||||||
<h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
<h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||||
<h2>${data['title']}</h2>
|
<h2>${data['title']}</h2>
|
||||||
% elif data['media_type'] == 'track':
|
% 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>
|
<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>
|
<h3 class="hidden-xs">T${data['media_index']}</h3>
|
||||||
% endif
|
% endif
|
||||||
@@ -371,7 +371,11 @@ DOCUMENTATION :: END
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-header">
|
<div class="table-card-header">
|
||||||
<div class="header-bar">
|
<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>
|
<span>Watch History for <strong>${data['title']}</strong></span>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -502,7 +506,7 @@ DOCUMENTATION :: END
|
|||||||
% elif data['media_type'] == 'album':
|
% elif data['media_type'] == 'album':
|
||||||
${data['parent_title']}<br />${data['title']}
|
${data['parent_title']}<br />${data['title']}
|
||||||
% elif data['media_type'] == 'track':
|
% 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
|
% endif
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -699,6 +703,10 @@ DOCUMENTATION :: END
|
|||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
<script>
|
<script>
|
||||||
|
$('.metadata-xml').on('tripleclick', function () {
|
||||||
|
openPlexXML("/library/metadata/${data['rating_key']}");
|
||||||
|
});
|
||||||
|
|
||||||
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
||||||
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
||||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||||
|
@@ -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-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-card-overlay">
|
||||||
<div class="item-children-overlay-text">
|
<div class="item-children-overlay-text">
|
||||||
Episode ${child['media_index']}
|
Episode ${child['media_index'] or child['originally_available_at']}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,16 +122,24 @@ DOCUMENTATION :: END
|
|||||||
% elif data['children_type'] == 'track':
|
% elif data['children_type'] == 'track':
|
||||||
% if loop.index % 2 == 0:
|
% if loop.index % 2 == 0:
|
||||||
<div class="item-children-list-item-even">
|
<div class="item-children-list-item-even">
|
||||||
<span class="item-children-list-item-index">${child['media_index']}</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></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}">
|
<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>
|
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="item-children-list-item-odd">
|
<div class="item-children-list-item-odd">
|
||||||
<span class="item-children-list-item-index">${child['media_index']}</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></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}">
|
<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>
|
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
||||||
</span>
|
</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>
|
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||||
% endif
|
% endif
|
||||||
<div class="item-children-instance-text-wrapper album-item">
|
<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['title']}">${child['title']}</h3>
|
||||||
<h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3>
|
<h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
function initConfigCheckbox(elem, toggleElem = null, reverse = false) {
|
function initConfigCheckbox(elem, toggleElem, reverse) {
|
||||||
|
toggleElem = (toggleElem === undefined) ? null : toggleElem;
|
||||||
|
reverse = (reverse === undefined) ? false : reverse;
|
||||||
var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
|
var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
|
||||||
config.css('overflow', 'hidden');
|
config.css('overflow', 'hidden');
|
||||||
if ($(elem).is(":checked")) {
|
if ($(elem).is(":checked")) {
|
||||||
@@ -36,7 +38,7 @@ function showMsg(msg, loader, timeout, ms, error) {
|
|||||||
var message = $("<div class='msg'>" + msg + "</div>");
|
var message = $("<div class='msg'>" + msg + "</div>");
|
||||||
if (loader) {
|
if (loader) {
|
||||||
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||||
feedback.css("padding", "14px 10px")
|
feedback.css("padding", "14px 10px");
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
feedback.css("background-color", "rgba(255,0,0,0.5)");
|
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').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
if (loader_msg) {
|
if (loader_msg) {
|
||||||
showMsg(loader_msg, true, false)
|
showMsg(loader_msg, true, false);
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
@@ -71,9 +73,9 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
|
|||||||
var result = $.parseJSON(xhr.responseText);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
var msg = result.message;
|
var msg = result.message;
|
||||||
if (result.result == 'success') {
|
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 {
|
} 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") {
|
if (typeof callback === "function") {
|
||||||
callback(result);
|
callback(result);
|
||||||
@@ -85,8 +87,8 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
|
|||||||
|
|
||||||
function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||||
// Set Message
|
// Set Message
|
||||||
feedback = (showMsg) ? $("#ajaxMsg") : $();
|
var feedback = (showMsg) ? $("#ajaxMsg") : $();
|
||||||
update = $("#updatebar");
|
var update = $("#updatebar");
|
||||||
if (update.is(":visible")) {
|
if (update.is(":visible")) {
|
||||||
var height = update.height() + 35;
|
var height = update.height() + 35;
|
||||||
feedback.css("bottom", height + "px");
|
feedback.css("bottom", height + "px");
|
||||||
@@ -96,8 +98,9 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
|||||||
feedback.fadeIn();
|
feedback.fadeIn();
|
||||||
// Get Form data
|
// Get Form data
|
||||||
var formID = "#" + url;
|
var formID = "#" + url;
|
||||||
if (form == true) {
|
var dataString;
|
||||||
var dataString = $(formID).serialize();
|
if (form === true) {
|
||||||
|
dataString = $(formID).serialize();
|
||||||
}
|
}
|
||||||
// Loader Image
|
// Loader Image
|
||||||
var loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
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');
|
var dataSucces = $(elem).data('success');
|
||||||
if (typeof dataSucces === "undefined") {
|
if (typeof dataSucces === "undefined") {
|
||||||
// Standard Message when variable is not set
|
// Standard Message when variable is not set
|
||||||
var dataSucces = "Success!";
|
dataSucces = "Success!";
|
||||||
}
|
}
|
||||||
// Data Errror Message
|
// Data Errror Message
|
||||||
var dataError = $(elem).data('error');
|
var dataError = $(elem).data('error');
|
||||||
if (typeof dataError === "undefined") {
|
if (typeof dataError === "undefined") {
|
||||||
// Standard Message when variable is not set
|
// 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
|
// Get Success & Error message from inline data, else use standard message
|
||||||
var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>");
|
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 (form) {
|
||||||
if ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ||
|
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) {
|
$('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) {
|
||||||
feedback.addClass('error')
|
feedback.addClass('error');
|
||||||
$(feedback).prepend(errorMsg);
|
$(feedback).prepend(errorMsg);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
errorMsg.fadeOut(function () {
|
errorMsg.fadeOut(function () {
|
||||||
@@ -128,7 +131,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
|||||||
feedback.fadeOut(function () {
|
feedback.fadeOut(function () {
|
||||||
feedback.removeClass('error');
|
feedback.removeClass('error');
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
$(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
|
$(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return false;
|
return false;
|
||||||
@@ -144,33 +147,33 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
|||||||
feedback.prepend(loader);
|
feedback.prepend(loader);
|
||||||
},
|
},
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
feedback.addClass('error')
|
feedback.addClass('error');
|
||||||
feedback.prepend(errorMsg);
|
feedback.prepend(errorMsg);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
errorMsg.fadeOut(function () {
|
errorMsg.fadeOut(function () {
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
feedback.fadeOut(function () {
|
feedback.fadeOut(function () {
|
||||||
feedback.removeClass('error')
|
feedback.removeClass('error');
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
success: function (data, jqXHR) {
|
success: function (data, jqXHR) {
|
||||||
feedback.prepend(succesMsg);
|
feedback.prepend(succesMsg);
|
||||||
feedback.addClass('success')
|
feedback.addClass('success');
|
||||||
setTimeout(function (e) {
|
setTimeout(function (e) {
|
||||||
succesMsg.fadeOut(function () {
|
succesMsg.fadeOut(function () {
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
feedback.fadeOut(function () {
|
feedback.fadeOut(function () {
|
||||||
feedback.removeClass('success');
|
feedback.removeClass('success');
|
||||||
});
|
});
|
||||||
if (reload == true) refreshSubmenu();
|
if (reload === true) refreshSubmenu();
|
||||||
if (reload == "table") {
|
if (reload === "table") {
|
||||||
refreshTable();
|
refreshTable();
|
||||||
}
|
}
|
||||||
if (reload == "tabs") refreshTab();
|
if (reload === "tabs") refreshTab();
|
||||||
if (reload == "page") location.reload();
|
if (reload === "page") location.reload();
|
||||||
if (reload == "submenu&table") {
|
if (reload === "submenu&table") {
|
||||||
refreshSubmenu();
|
refreshSubmenu();
|
||||||
refreshTable();
|
refreshTable();
|
||||||
}
|
}
|
||||||
@@ -179,7 +182,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
|||||||
$(formID + " select").children('option[disabled=disabled]').attr(
|
$(formID + " select").children('option[disabled=disabled]').attr(
|
||||||
'selected', 'selected');
|
'selected', 'selected');
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
complete: function (jqXHR, textStatus) {
|
complete: function (jqXHR, textStatus) {
|
||||||
@@ -215,19 +218,20 @@ function isPrivateIP(ip_address) {
|
|||||||
|
|
||||||
$.cachedScript('js/ipaddr.min.js').done(function () {
|
$.cachedScript('js/ipaddr.min.js').done(function () {
|
||||||
if (ipaddr.isValid(ip_address)) {
|
if (ipaddr.isValid(ip_address)) {
|
||||||
var addr = ipaddr.process(ip_address)
|
var addr = ipaddr.process(ip_address);
|
||||||
|
|
||||||
|
var rangeList = [];
|
||||||
if (addr.kind() === 'ipv4') {
|
if (addr.kind() === 'ipv4') {
|
||||||
var rangeList = [
|
rangeList = [
|
||||||
ipaddr.parseCIDR('127.0.0.0/8'),
|
ipaddr.parseCIDR('127.0.0.0/8'),
|
||||||
ipaddr.parseCIDR('10.0.0.0/8'),
|
ipaddr.parseCIDR('10.0.0.0/8'),
|
||||||
ipaddr.parseCIDR('172.16.0.0/12'),
|
ipaddr.parseCIDR('172.16.0.0/12'),
|
||||||
ipaddr.parseCIDR('192.168.0.0/16')
|
ipaddr.parseCIDR('192.168.0.0/16')
|
||||||
]
|
];
|
||||||
} else {
|
} else {
|
||||||
var rangeList = [
|
rangeList = [
|
||||||
ipaddr.parseCIDR('fd00::/8')
|
ipaddr.parseCIDR('fd00::/8')
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
|
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
|
||||||
@@ -238,12 +242,13 @@ function isPrivateIP(ip_address) {
|
|||||||
} else {
|
} else {
|
||||||
defer.resolve('n/a');
|
defer.resolve('n/a');
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return defer.promise();
|
return defer.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanTime(seconds) {
|
function humanTime(seconds) {
|
||||||
|
var text;
|
||||||
if (seconds >= 86400) {
|
if (seconds >= 86400) {
|
||||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' + '<h3>' +
|
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' + '<h3>' +
|
||||||
Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' + '<h3>' +
|
Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' + '<h3>' +
|
||||||
@@ -265,6 +270,7 @@ function humanTime(seconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function humanTimeClean(seconds) {
|
function humanTimeClean(seconds) {
|
||||||
|
var text;
|
||||||
if (seconds >= 86400) {
|
if (seconds >= 86400) {
|
||||||
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + Math.floor(moment.duration((
|
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + Math.floor(moment.duration((
|
||||||
seconds % 86400), 'seconds').asHours()) + ' hrs ' + 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++) {
|
for (var i = 0; i < ca.length; i++) {
|
||||||
var c = ca[i];
|
var c = ca[i];
|
||||||
while (c.charAt(0) == ' ') c = c.substring(1);
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -354,24 +360,24 @@ var Accordion = function (el, multiple) {
|
|||||||
links.on('click', {
|
links.on('click', {
|
||||||
el: this.el,
|
el: this.el,
|
||||||
multiple: this.multiple
|
multiple: this.multiple
|
||||||
}, this.dropdown)
|
}, this.dropdown);
|
||||||
}
|
};
|
||||||
Accordion.prototype.dropdown = function (e) {
|
Accordion.prototype.dropdown = function (e) {
|
||||||
var $el = e.data.el;
|
var $el = e.data.el;
|
||||||
$this = $(this),
|
$this = $(this);
|
||||||
$next = $this.next();
|
$next = $this.next();
|
||||||
$next.slideToggle();
|
$next.slideToggle();
|
||||||
$this.parent().toggleClass('open');
|
$this.parent().toggleClass('open');
|
||||||
if (!e.data.multiple) {
|
if (!e.data.multiple) {
|
||||||
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function clearSearchButton(tableName, table) {
|
function clearSearchButton(tableName, table) {
|
||||||
$('#' + tableName + '_filter').find('input[type=search]').wrap(
|
$('#' + tableName + '_filter').find('input[type=search]').wrap(
|
||||||
'<div class="input-group" role="group" aria-label="Search"></div>').after(
|
'<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-' +
|
'<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 () {
|
$('#clear-search-' + tableName).click(function () {
|
||||||
table.search('').draw();
|
table.search('').draw();
|
||||||
});
|
});
|
||||||
@@ -401,7 +407,6 @@ $('*').on('click', '.refresh_pms_image', function (e) {
|
|||||||
} else {
|
} else {
|
||||||
if (pms_proxy_url.indexOf('refresh=true') > -1) {
|
if (pms_proxy_url.indexOf('refresh=true') > -1) {
|
||||||
pms_proxy_url = pms_proxy_url.replace("&refresh=true", "");
|
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 + ')');
|
||||||
background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)');
|
background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)');
|
||||||
} else {
|
} else {
|
||||||
@@ -416,8 +421,7 @@ function humanFileSize(bytes, si) {
|
|||||||
if (Math.abs(bytes) < thresh) {
|
if (Math.abs(bytes) < thresh) {
|
||||||
return bytes + ' B';
|
return bytes + ' B';
|
||||||
}
|
}
|
||||||
var units = si
|
var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
||||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
var u = -1;
|
var u = -1;
|
||||||
do {
|
do {
|
||||||
@@ -436,10 +440,10 @@ function forceMinMax(elem) {
|
|||||||
if (isNaN(val)) {
|
if (isNaN(val)) {
|
||||||
elem.val(default_val);
|
elem.val(default_val);
|
||||||
}
|
}
|
||||||
else if (min != undefined && val < min) {
|
else if (min !== undefined && val < min) {
|
||||||
elem.val(min);
|
elem.val(min);
|
||||||
}
|
}
|
||||||
else if (max != undefined && val > max) {
|
else if (max !== undefined && val > max) {
|
||||||
elem.val(max);
|
elem.val(max);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -453,4 +457,11 @@ function capitalizeFirstLetter(string) {
|
|||||||
|
|
||||||
$.fn.slideToggleBool = function(bool, options) {
|
$.fn.slideToggleBool = function(bool, options) {
|
||||||
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
|
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
function openPlexXML(endpoint, plextv, params) {
|
||||||
|
var data = $.extend({endpoint: endpoint, plextv: plextv}, params);
|
||||||
|
$.getJSON('return_plex_xml_url', data, function(xml_url) {
|
||||||
|
window.open(xml_url, '_blank');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-book"></i> All Libraries</span>
|
<span id="libraries-xml"><i class="fa fa-book"></i> All Libraries</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -198,5 +198,9 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
$('#libraries-xml').on('tripleclick', function () {
|
||||||
|
openPlexXML('/library/sections/all');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
@@ -11,12 +11,11 @@ DOCUMENTATION :: END
|
|||||||
|
|
||||||
<ul class="stacked-configs list-unstyled">
|
<ul class="stacked-configs list-unstyled">
|
||||||
% for device in sorted(devices_list, key=lambda k: k['device_name']):
|
% 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>
|
||||||
<!--<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-fw fa-mobile"></i></span>
|
||||||
<span class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span>
|
|
||||||
${device['friendly_name'] or device['device_name']} <span class="friendly_name">(${device['id']})</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']}">
|
<span class="toggle-right friendly_name" id="device-last_seen-${device['id']}">
|
||||||
% if device['last_seen']:
|
% if device['last_seen']:
|
||||||
<script>
|
<script>
|
||||||
@@ -26,14 +25,13 @@ DOCUMENTATION :: END
|
|||||||
never
|
never
|
||||||
% endif
|
% endif
|
||||||
</span>
|
</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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
% endfor
|
% 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>
|
||||||
<span class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span> Register a new device
|
<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-plus"></i></span>
|
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
@@ -12,15 +12,15 @@ DOCUMENTATION :: END
|
|||||||
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %>
|
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %>
|
||||||
<ul class="stacked-configs list-unstyled">
|
<ul class="stacked-configs list-unstyled">
|
||||||
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
|
% 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>
|
||||||
<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']:
|
% if newsletter['friendly_name']:
|
||||||
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
|
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
|
||||||
% else:
|
% else:
|
||||||
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']})</span>
|
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']})</span>
|
||||||
% endif
|
% 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']}">
|
<span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}">
|
||||||
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
|
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
|
||||||
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
|
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
|
||||||
@@ -32,10 +32,10 @@ DOCUMENTATION :: END
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
% endfor
|
% 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>
|
||||||
<span class="toggle-left"><i class="fa fa-lg fa-newspaper-o"></i></span> Add a new newsletter agent
|
<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-plus"></i></span>
|
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
% if notifier:
|
% if notifier:
|
||||||
<%!
|
<%!
|
||||||
import json
|
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()
|
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']]
|
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':
|
% elif item['input_type'] == 'checkbox':
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<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>
|
</label>
|
||||||
<p class="help-block">${item['description'] | n}</p>
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||||
@@ -146,7 +147,7 @@
|
|||||||
% for action in available_notification_actions:
|
% for action in available_notification_actions:
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<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>
|
</label>
|
||||||
<p class="help-block">${action['description'] | n}</p>
|
<p class="help-block">${action['description'] | n}</p>
|
||||||
<input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}">
|
<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">
|
<ul class="stacked-configs list-unstyled">
|
||||||
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])):
|
% 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>
|
||||||
<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']:
|
% if notifier['friendly_name']:
|
||||||
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
|
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
|
||||||
% else:
|
% else:
|
||||||
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']})</span>
|
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']})</span>
|
||||||
% endif
|
% 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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
% endfor
|
% 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>
|
||||||
<span class="toggle-left"><i class="fa fa-lg fa-bell"></i></span> Add a new notification agent
|
<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-plus"></i></span>
|
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -115,9 +115,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox advanced-setting">
|
<div class="checkbox advanced-setting">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Successive Play History
|
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Play History
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Group successive play history by the same user as a single entry in the watch statistics, tables, and graphs.</p>
|
<p class="help-block">Group play history for the same item and user as a single entry when progress is less than the watched percent.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox advanced-setting">
|
<div class="checkbox advanced-setting">
|
||||||
<label>
|
<label>
|
||||||
@@ -646,11 +646,11 @@
|
|||||||
<div role="tabpanel" class="tab-pane" id="tabs-plex_media_server">
|
<div role="tabpanel" class="tab-pane" id="tabs-plex_media_server">
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
<h3>Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
|
<h3 id="resources-xml">Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group has-feedback" id="pms_ip_group">
|
<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="row">
|
||||||
<div class="col-md-9" id="selectize-pms-ip-container">
|
<div class="col-md-9" id="selectize-pms-ip-container">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -965,12 +965,46 @@
|
|||||||
<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>
|
<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>
|
||||||
<div id="self_host_newsletter_options" style="overlfow: hidden; display: ${'block' if config['newsletter_self_hosted'] == 'checked' else 'none'}">
|
<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">
|
<div class="form-group">
|
||||||
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
|
<p class="help-block" id="self_host_newsletter_message">
|
||||||
</p>
|
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
|
||||||
<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>
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
<div class="form-group advanced-setting">
|
<div class="form-group advanced-setting">
|
||||||
<label for="newsletter_dir">Custom Newsletter Templates Folder</label>
|
<label for="newsletter_dir">Custom Newsletter Templates Folder</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -998,21 +1032,30 @@
|
|||||||
<label for="notify_upload_posters">Image Hosting</label>
|
<label for="notify_upload_posters">Image Hosting</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<select class="form-control" id="notify_upload_posters" name="notify_upload_posters">
|
<div class="${'input-group' if config['notify_upload_posters'] in (1, 3) else ''}">
|
||||||
<option value="0" ${'selected' if config['notify_upload_posters'] == 0 else ''}>Disabled</option>
|
<select class="form-control" id="notify_upload_posters" name="notify_upload_posters">
|
||||||
<option value="1" ${'selected' if config['notify_upload_posters'] == 1 else ''}>Imgur</option>
|
<option value="0" ${'selected' if config['notify_upload_posters'] == 0 else ''}>Disabled</option>
|
||||||
<option value="3" ${'selected' if config['notify_upload_posters'] == 3 else ''}>Cloudinary</option>
|
<option value="1" ${'selected' if config['notify_upload_posters'] == 1 else ''}>Imgur</option>
|
||||||
<option value="2" ${'selected' if config['notify_upload_posters'] == 2 else ''}>Self-hosted on public Tautulli domain</option>
|
<option value="3" ${'selected' if config['notify_upload_posters'] == 3 else ''}>Cloudinary</option>
|
||||||
</select>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Select where to host Plex images for notifications and newsletters.</p>
|
<p class="help-block">Select where to host Plex images for notifications and newsletters.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
<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">
|
<div class="form-group">
|
||||||
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
|
<p class="help-block" id="imgur_upload_message">
|
||||||
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
|
||||||
</p>
|
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="imgur_client_id">Imgur Client ID</label>
|
<label for="imgur_client_id">Imgur Client ID</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -1024,13 +1067,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
|
<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>
|
<div class="form-group">
|
||||||
<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>
|
<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>
|
||||||
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
<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">
|
<div class="form-group">
|
||||||
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
|
<p class="help-block" id="imgur_upload_message">
|
||||||
</p>
|
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cloudinary_cloud_name">Cloudinary Cloud Name</label>
|
<label for="cloudinary_cloud_name">Cloudinary Cloud Name</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -1221,7 +1268,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="form-group">
|
<div class="form-group">
|
||||||
<label>Registered Devices</label>
|
<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 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>
|
<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>
|
||||||
@@ -1376,7 +1423,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ul class="stacked-configs list-unstyled">
|
<ul class="stacked-configs list-unstyled">
|
||||||
% for agent in sorted(available_notification_agents, key=lambda k: k['label'].lower()):
|
% 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>
|
<span>${agent['label']}</span>
|
||||||
</li>
|
</li>
|
||||||
% endfor
|
% endfor
|
||||||
@@ -1404,7 +1451,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ul class="stacked-configs list-unstyled">
|
<ul class="stacked-configs list-unstyled">
|
||||||
% for agent in available_newsletter_agents:
|
% 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>
|
<span>${agent['label']}</span>
|
||||||
</li>
|
</li>
|
||||||
% endfor
|
% endfor
|
||||||
@@ -1746,6 +1793,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadNotifierConfig(notifier_id) {
|
function loadNotifierConfig(notifier_id) {
|
||||||
|
showMsg('<i class="fa fa-refresh fa-spin"></i> Loading Configuration', false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_notifier_config_modal',
|
url: 'get_notifier_config_modal',
|
||||||
data: { notifier_id: notifier_id },
|
data: { notifier_id: notifier_id },
|
||||||
@@ -1753,6 +1801,7 @@
|
|||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
$("#notifier-config-modal").html(xhr.responseText).modal('show');
|
$("#notifier-config-modal").html(xhr.responseText).modal('show');
|
||||||
|
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1769,6 +1818,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadNewsletterConfig(newsletter_id) {
|
function loadNewsletterConfig(newsletter_id) {
|
||||||
|
showMsg('<i class="fa fa-refresh fa-spin"></i> Loading Configuration', false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_newsletter_config_modal',
|
url: 'get_newsletter_config_modal',
|
||||||
data: { newsletter_id: newsletter_id },
|
data: { newsletter_id: newsletter_id },
|
||||||
@@ -1776,6 +1826,7 @@
|
|||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
$("#newsletter-config-modal").html(xhr.responseText).modal('show');
|
$("#newsletter-config-modal").html(xhr.responseText).modal('show');
|
||||||
|
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1792,6 +1843,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadMobileDeviceConfig(mobile_device_id) {
|
function loadMobileDeviceConfig(mobile_device_id) {
|
||||||
|
showMsg('<i class="fa fa-refresh fa-spin"></i> Loading Configuration', false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_mobile_device_config_modal',
|
url: 'get_mobile_device_config_modal',
|
||||||
data: { mobile_device_id: mobile_device_id },
|
data: { mobile_device_id: mobile_device_id },
|
||||||
@@ -1799,6 +1851,7 @@
|
|||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
$("#mobile-device-config-modal").html(xhr.responseText).modal('show');
|
$("#mobile-device-config-modal").html(xhr.responseText).modal('show');
|
||||||
|
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2305,7 +2358,7 @@ $(document).ready(function() {
|
|||||||
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||||
async: true,
|
async: true,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
if (data !== 'true') {
|
if (data === 'false' || data === '0') {
|
||||||
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||||
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
|
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
|
||||||
}
|
}
|
||||||
@@ -2440,6 +2493,7 @@ $(document).ready(function() {
|
|||||||
$("#allow_guest_access").attr("disabled", false);
|
$("#allow_guest_access").attr("disabled", false);
|
||||||
$("#allowGuestCheck").html("");
|
$("#allowGuestCheck").html("");
|
||||||
}
|
}
|
||||||
|
newsletterPasswordEnabled();
|
||||||
}
|
}
|
||||||
allowGuestAccessCheck();
|
allowGuestAccessCheck();
|
||||||
|
|
||||||
@@ -2543,7 +2597,7 @@ $(document).ready(function() {
|
|||||||
var result = $.parseJSON(xhr.responseText);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
var msg = result.message;
|
var msg = result.message;
|
||||||
$('#add-notifier-modal').modal('hide');
|
$('#add-notifier-modal').modal('hide');
|
||||||
if (result.result == 'success') {
|
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);
|
||||||
loadNotifierConfig(result.notifier_id);
|
loadNotifierConfig(result.notifier_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -2565,7 +2619,7 @@ $(document).ready(function() {
|
|||||||
var result = $.parseJSON(xhr.responseText);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
var msg = result.message;
|
var msg = result.message;
|
||||||
$('#add-newsletter-modal').modal('hide');
|
$('#add-newsletter-modal').modal('hide');
|
||||||
if (result.result == 'success') {
|
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);
|
||||||
loadNewsletterConfig(result.newsletter_id);
|
loadNewsletterConfig(result.newsletter_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -2606,11 +2660,37 @@ $(document).ready(function() {
|
|||||||
} else {
|
} else {
|
||||||
$('#cloudinary_upload_options').slideUp();
|
$('#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 () {
|
$('#notify_upload_posters').change(function () {
|
||||||
imageUpload();
|
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() {
|
function baseURLSet() {
|
||||||
if ($('#http_base_url').val()) {
|
if ($('#http_base_url').val()) {
|
||||||
$('.base-url-warning').hide();
|
$('.base-url-warning').hide();
|
||||||
@@ -2638,6 +2718,28 @@ $(document).ready(function() {
|
|||||||
newsletterUploadEnabled();
|
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 () {
|
$('body').on('click', 'a[data-tab-destination]', function () {
|
||||||
var tab = $(this).data('tab-destination');
|
var tab = $(this).data('tab-destination');
|
||||||
$("a[href=#" + tab + "]").click();
|
$("a[href=#" + tab + "]").click();
|
||||||
@@ -2651,6 +2753,10 @@ $(document).ready(function() {
|
|||||||
body_container.animate({scrollTop: scroll_pos});
|
body_container.animate({scrollTop: scroll_pos});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#resources-xml').on('tripleclick', function () {
|
||||||
|
openPlexXML('/api/resources', true, {includeHttps: 1});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
@@ -46,8 +46,10 @@ DOCUMENTATION :: END
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<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">
|
<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>
|
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:
|
% else:
|
||||||
Stream Info: <strong>${data['title']} (${user})</strong>
|
Stream Info: <strong>${data['title']} (${user})</strong>
|
||||||
% endif
|
% endif
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-cloud-download"></i> Synced Items</span>
|
<span id="sync-xml"><i class="fa fa-cloud-download"></i> Synced Items</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -185,5 +185,9 @@
|
|||||||
$("#refresh-syncs-list").click(function() {
|
$("#refresh-syncs-list").click(function() {
|
||||||
sync_table.ajax.reload();
|
sync_table.ajax.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#sync-xml').on('tripleclick', function () {
|
||||||
|
openPlexXML('/servers/{machine_id}/sync_lists', true);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
@@ -108,8 +108,8 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="dashboard-recent-media-metacontainer">
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
<h3 title="${item['grandparent_title']}">
|
<h3 title="${item['original_title'] or item['grandparent_title']}">
|
||||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
<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>
|
||||||
<h3 class="text-muted" title="${item['title']}">
|
<h3 class="text-muted" title="${item['title']}">
|
||||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-group"></i> All Users</span>
|
<span id="users-xml"><i class="fa fa-group"></i> All Users</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -202,5 +202,9 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
$('#users-xml').on('tripleclick', function () {
|
||||||
|
openPlexXML('/api/users', true);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
@@ -13,9 +13,9 @@
|
|||||||
|
|
||||||
service = get_img_service(include_self=True)
|
service = get_img_service(include_self=True)
|
||||||
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
||||||
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/image/'
|
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/'
|
||||||
elif service and service != 'self-hosted' and preview:
|
elif preview and service and service != 'self-hosted':
|
||||||
base_url_image = 'newsletter/image/'
|
base_url_image = 'image/'
|
||||||
else:
|
else:
|
||||||
base_url_image = ''
|
base_url_image = ''
|
||||||
|
|
||||||
@@ -68,17 +68,14 @@
|
|||||||
width: 100%;
|
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 {
|
.container {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto !important;
|
margin: 0 auto !important;
|
||||||
/* makes it centered */
|
|
||||||
max-width: 1042px;
|
max-width: 1042px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 1042px;
|
width: 1042px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
|
||||||
.content {
|
.content {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -172,88 +169,9 @@
|
|||||||
text-decoration: underline;
|
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
|
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 {
|
.mb5 {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
@@ -263,9 +181,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preheader {
|
.preheader {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
@@ -324,6 +239,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------
|
/* -------------------------------------
|
||||||
MEDIA SECTIONS
|
MEDIA SECTIONS
|
||||||
------------------------------------- */
|
------------------------------------- */
|
||||||
@@ -378,6 +294,7 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------
|
/* -------------------------------------
|
||||||
MEDIA CARDS
|
MEDIA CARDS
|
||||||
------------------------------------- */
|
------------------------------------- */
|
||||||
@@ -480,21 +397,21 @@
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
.card-info-footer .badge-container {
|
.card-info-footer .badge-container {
|
||||||
max-width: 265px;
|
max-width: 260px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.card-info-footer .star-rating-container {
|
.card-info-footer .star-rating-container {
|
||||||
width: 60px;
|
width: 65px;
|
||||||
vertical-align: bottom;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
}
|
||||||
.card-info-footer .star-rating {
|
.card-info-footer .star-rating {
|
||||||
|
margin-left: 4px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
.star-rating.full {
|
.star-rating.full {
|
||||||
color: #E5A00D;
|
color: #E5A00D;
|
||||||
@@ -505,6 +422,7 @@
|
|||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 10px;
|
min-width: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -609,21 +527,12 @@
|
|||||||
line-height: inherit !important;
|
line-height: inherit !important;
|
||||||
text-decoration: none !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>
|
</style>
|
||||||
</head>
|
</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:
|
% 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 -->
|
<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:
|
% 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 -->
|
<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 -->
|
||||||
@@ -632,19 +541,13 @@
|
|||||||
<tr>
|
<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;">
|
<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;">
|
<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>
|
<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:
|
% 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 -->
|
<div class="view-full" style="clear: both;color: #282A2D;font-size: 12px;margin-bottom: 10px;text-align: center;width: 100%;"> <!-- IGNORE SAVE -->
|
||||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||||
</div> <!-- IGNORE SAVE -->
|
</div> <!-- IGNORE SAVE -->
|
||||||
% endif
|
% 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;">
|
<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>
|
<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;">
|
<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;">
|
<div class="header" style="width: 100%;height: 90px;text-align: center;">
|
||||||
@@ -654,7 +557,6 @@
|
|||||||
<div class="dates" style="color: #aaaaaa;font-size: 20px;text-align: center;">${parameters['start_date']} - ${parameters['end_date']}</div>
|
<div class="dates" style="color: #aaaaaa;font-size: 20px;text-align: center;">${parameters['start_date']} - ${parameters['end_date']}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
% if message:
|
% if message:
|
||||||
<tr>
|
<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;">
|
<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;">
|
||||||
@@ -663,7 +565,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if recently_added.get('movie'):
|
% if recently_added.get('movie'):
|
||||||
<tr>
|
<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;">
|
<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;">
|
||||||
@@ -679,109 +580,105 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<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%;">
|
<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):
|
||||||
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
<tr>
|
||||||
<tr>
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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%;">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
<tr>
|
||||||
<tbody>
|
% for movie in (movie_a, movie_b):
|
||||||
<tr>
|
% if movie:
|
||||||
% for movie in (movie_a, movie_b):
|
% if not movie_b:
|
||||||
% if movie:
|
<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>
|
||||||
% if not movie_b:
|
% endif
|
||||||
<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>
|
<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;">
|
||||||
% endif
|
<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);">
|
||||||
<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;">
|
<tr>
|
||||||
<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);">
|
<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;">
|
||||||
<tbody>
|
<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>
|
<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;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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);">
|
<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;">
|
||||||
<tbody>
|
<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;">
|
||||||
<tr>
|
</a>
|
||||||
<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>
|
</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;">
|
</tr>
|
||||||
<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%;">
|
</table>
|
||||||
<tbody>
|
</td>
|
||||||
<tr>
|
<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;">
|
||||||
<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;">
|
<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%;">
|
||||||
<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>
|
<tr>
|
||||||
</td>
|
<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;">
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</td>
|
||||||
<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%;">
|
</tr>
|
||||||
% if movie['tagline']:
|
<tr>
|
||||||
<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;">
|
<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%;">
|
||||||
<em>${movie['tagline']}</em>
|
% if movie['tagline']:
|
||||||
</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;">
|
||||||
% endif
|
<em>${movie['tagline']}</em>
|
||||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
</p>
|
||||||
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
% endif
|
||||||
</p>
|
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||||
</td>
|
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
||||||
</tr>
|
</p>
|
||||||
<tr>
|
</td>
|
||||||
<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;">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
<tr>
|
||||||
<tbody>
|
<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;">
|
||||||
<tr>
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
<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;">
|
<tr>
|
||||||
% if movie['year']:
|
<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;">
|
||||||
<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>
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
% endif
|
<tr>
|
||||||
% if movie['duration']:
|
% if movie['year']:
|
||||||
<% duration = int(int(movie['duration'])/60000) %>
|
<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>
|
||||||
<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
|
||||||
% endif
|
% if movie['duration']:
|
||||||
% if movie['genres']:
|
<% duration = int(int(movie['duration'])/60000) %>
|
||||||
% for genre in movie['genres'][:2]:
|
<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>
|
||||||
<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>
|
% endif
|
||||||
% endfor
|
% if movie['genres']:
|
||||||
% endif
|
% for genre in movie['genres'][:]:
|
||||||
</td>
|
<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>
|
||||||
% if movie['rating']:
|
% endfor
|
||||||
<% rating = int(round(float(movie['rating']) / 2)) %>
|
% endif
|
||||||
<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;">
|
</tr>
|
||||||
% for _ in range(rating):
|
</table>
|
||||||
<span class="star-rating full" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</span>
|
</td>
|
||||||
% endfor
|
% if movie['rating']:
|
||||||
% for _ in range(5-rating):
|
<% rating = int(round(float(movie['rating']) / 2)) %>
|
||||||
<span class="star-rating empty" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</span>
|
<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;">
|
||||||
% endfor
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
</td>
|
<tr>
|
||||||
% endif
|
% for _ in range(rating):
|
||||||
</tr>
|
<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>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
% for _ in range(5-rating):
|
||||||
</td>
|
<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>
|
||||||
</tr>
|
% endfor
|
||||||
</tbody>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
% if not movie_b:
|
</table>
|
||||||
<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>
|
</td>
|
||||||
% endif
|
% if not movie_b:
|
||||||
% endif
|
<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>
|
||||||
% endfor
|
% endif
|
||||||
</tr>
|
% endif
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
% endfor
|
</tr>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -803,142 +700,137 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<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%;">
|
<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):
|
||||||
% for show_a, show_b in grouper(recently_added['show'], 2):
|
<tr>
|
||||||
<tr>
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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%;">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
<tr>
|
||||||
<tbody>
|
% for show in (show_a, show_b):
|
||||||
<tr>
|
% if show:
|
||||||
% for show in (show_a, show_b):
|
% if not show_b:
|
||||||
% if show:
|
<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>
|
||||||
% if not show_b:
|
% endif
|
||||||
<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:
|
||||||
if show['season_count'] == 1:
|
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||||
if show['season'][0]['episode_count'] == 1:
|
else:
|
||||||
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
||||||
else:
|
else:
|
||||||
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
link_rating_key = show['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);">
|
||||||
<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;">
|
<tr>
|
||||||
<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);">
|
<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;">
|
||||||
<tbody>
|
<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>
|
<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;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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);">
|
<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;">
|
||||||
<tbody>
|
<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;">
|
||||||
<tr>
|
</a>
|
||||||
<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>
|
</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;">
|
</tr>
|
||||||
<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%;">
|
</table>
|
||||||
<tbody>
|
</td>
|
||||||
<tr>
|
<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;">
|
||||||
<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;">
|
<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%;">
|
||||||
<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>
|
<tr>
|
||||||
</td>
|
<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;">
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</td>
|
||||||
<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%;">
|
</tr>
|
||||||
<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;">
|
<tr>
|
||||||
% if show['season_count'] > 1:
|
<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%;">
|
||||||
<em>${show['season_count']} seasons /</em>
|
<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
|
% endif
|
||||||
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
% if show['duration']:
|
||||||
<em>${total_show_episodes} episode${'s' if total_show_episodes > 1 else ''}</em>
|
<% duration = int(int(show['duration'])/60000) %>
|
||||||
</p>
|
<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>
|
||||||
<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;">
|
% endif
|
||||||
% for i, season in enumerate(show['season'][:8]):
|
% if show['genres']:
|
||||||
Season ${season['media_index']} ·
|
% for genre in show['genres'][:2]:
|
||||||
% if season['episode_count'] == 1:
|
<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>
|
||||||
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
|
|
||||||
% endfor
|
% 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
|
% endif
|
||||||
</p>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
% if show['rating']:
|
||||||
<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;">
|
<% rating = int(round(float(show['rating']) / 2)) %>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
<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;">
|
||||||
<tbody>
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
<tr>
|
<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;">
|
% for _ in range(rating):
|
||||||
% if show['year']:
|
<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>
|
||||||
<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>
|
% endfor
|
||||||
% endif
|
% for _ in range(5-rating):
|
||||||
% if show['duration']:
|
<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>
|
||||||
<% duration = int(int(show['duration'])/60000) %>
|
% endfor
|
||||||
<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>
|
</tr>
|
||||||
% endif
|
</table>
|
||||||
% if show['genres']:
|
</td>
|
||||||
% for genre in show['genres'][:2]:
|
% endif
|
||||||
<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>
|
</tr>
|
||||||
% 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>
|
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
% if not show_b:
|
</table>
|
||||||
<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>
|
</td>
|
||||||
% endif
|
% if not show_b:
|
||||||
% endif
|
<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>
|
||||||
% endfor
|
% endif
|
||||||
</tr>
|
% endif
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
% endfor
|
</tr>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -960,124 +852,114 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<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%;">
|
<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):
|
||||||
% for album_a, album_b in grouper([a for artist in recently_added['artist'] for a in artist['album']], 2):
|
<tr>
|
||||||
<tr>
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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%;">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
<tr>
|
||||||
<tbody>
|
% for album in (album_a, album_b):
|
||||||
<tr>
|
% if album:
|
||||||
% for album in (album_a, album_b):
|
% if not album_b:
|
||||||
% if album:
|
<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>
|
||||||
% if not album_b:
|
% endif
|
||||||
<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>
|
<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;">
|
||||||
% endif
|
<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);">
|
||||||
<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;">
|
<tr>
|
||||||
<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);">
|
<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;">
|
||||||
<tbody>
|
<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>
|
<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;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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);">
|
<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;">
|
||||||
<tbody>
|
<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;">
|
||||||
<tr>
|
</a>
|
||||||
<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>
|
</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;">
|
</tr>
|
||||||
<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%;">
|
</table>
|
||||||
<tbody>
|
</td>
|
||||||
<tr>
|
<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;">
|
||||||
<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;">
|
<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%;">
|
||||||
<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>
|
<tr>
|
||||||
</td>
|
<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;">
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</td>
|
||||||
<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;">
|
</tr>
|
||||||
<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;">
|
<tr>
|
||||||
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
<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>
|
<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 artist['title'].lower() != 'various artists':
|
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
||||||
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
</p>
|
||||||
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
% if album['parent_title'].lower() != 'various artists':
|
||||||
</p>
|
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
|
||||||
% endif
|
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
||||||
</td>
|
</p>
|
||||||
</tr>
|
% endif
|
||||||
<tr>
|
</td>
|
||||||
<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;">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
<tr>
|
||||||
<tbody>
|
<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;">
|
||||||
<tr>
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
<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;">
|
<tr>
|
||||||
% if album['year']:
|
<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;">
|
||||||
<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>
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
% endif
|
<tr>
|
||||||
% if album['genres']:
|
% if album['year']:
|
||||||
% for genre in album['genres'][:2]:
|
<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>
|
||||||
<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>
|
% endif
|
||||||
% endfor
|
% if album['genres']:
|
||||||
% endif
|
% for genre in album['genres'][:2]:
|
||||||
</td>
|
<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>
|
||||||
% if album['rating']:
|
% endfor
|
||||||
<% rating = int(round(float(album['rating']) / 2)) %>
|
% endif
|
||||||
<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;">
|
</tr>
|
||||||
% for _ in range(rating):
|
</table>
|
||||||
<span class="star-rating full" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #E5A00D;">★</span>
|
</td>
|
||||||
% endfor
|
% if album['rating']:
|
||||||
% for _ in range(5-rating):
|
<% rating = int(round(float(album['rating']) / 2)) %>
|
||||||
<span class="star-rating empty" style="font-size: 0.8rem;line-height: 1rem;width: 0.5rem;display: inline-block;color: #aaaaaa;">☆</span>
|
<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;">
|
||||||
% endfor
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
|
||||||
</td>
|
<tr>
|
||||||
% endif
|
% for _ in range(rating):
|
||||||
</tr>
|
<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>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
% for _ in range(5-rating):
|
||||||
</td>
|
<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>
|
||||||
</tr>
|
% endfor
|
||||||
</tbody>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
% if not album_b:
|
</table>
|
||||||
<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>
|
</td>
|
||||||
% endif
|
% if not album_b:
|
||||||
% endif
|
<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>
|
||||||
% endfor
|
% endif
|
||||||
</tr>
|
% endif
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
% endfor
|
</tr>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<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%;">
|
<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="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;">
|
<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>
|
</div>
|
||||||
<!-- END FOOTER -->
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- END MAIN CONTENT AREA -->
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- END CENTERED WHITE CONTAINER -->
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@@ -13,9 +13,9 @@
|
|||||||
|
|
||||||
service = get_img_service(include_self=True)
|
service = get_img_service(include_self=True)
|
||||||
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
||||||
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/image/'
|
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/'
|
||||||
elif service and service != 'self-hosted' and preview:
|
elif preview and service and service != 'self-hosted':
|
||||||
base_url_image = 'newsletter/image/'
|
base_url_image = 'image/'
|
||||||
else:
|
else:
|
||||||
base_url_image = ''
|
base_url_image = ''
|
||||||
|
|
||||||
@@ -68,17 +68,14 @@
|
|||||||
width: 100%;
|
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 {
|
.container {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto !important;
|
margin: 0 auto !important;
|
||||||
/* makes it centered */
|
|
||||||
max-width: 1042px;
|
max-width: 1042px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 1042px;
|
width: 1042px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
|
||||||
.content {
|
.content {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -172,88 +169,9 @@
|
|||||||
text-decoration: underline;
|
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
|
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 {
|
.mb5 {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
@@ -263,9 +181,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preheader {
|
.preheader {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
@@ -324,6 +239,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------
|
/* -------------------------------------
|
||||||
MEDIA SECTIONS
|
MEDIA SECTIONS
|
||||||
------------------------------------- */
|
------------------------------------- */
|
||||||
@@ -378,6 +294,7 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------
|
/* -------------------------------------
|
||||||
MEDIA CARDS
|
MEDIA CARDS
|
||||||
------------------------------------- */
|
------------------------------------- */
|
||||||
@@ -451,6 +368,7 @@
|
|||||||
line-height: 1.2rem;
|
line-height: 1.2rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
.card-info-title a {
|
.card-info-title a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -480,21 +398,21 @@
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
.card-info-footer .badge-container {
|
.card-info-footer .badge-container {
|
||||||
max-width: 265px;
|
max-width: 260px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.card-info-footer .star-rating-container {
|
.card-info-footer .star-rating-container {
|
||||||
width: 60px;
|
width: 65px;
|
||||||
vertical-align: bottom;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
}
|
||||||
.card-info-footer .star-rating {
|
.card-info-footer .star-rating {
|
||||||
|
margin-left: 4px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
.star-rating.full {
|
.star-rating.full {
|
||||||
color: #E5A00D;
|
color: #E5A00D;
|
||||||
@@ -505,6 +423,7 @@
|
|||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 10px;
|
min-width: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -609,21 +528,12 @@
|
|||||||
line-height: inherit !important;
|
line-height: inherit !important;
|
||||||
text-decoration: none !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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="">
|
<body>
|
||||||
% if preview and service:
|
% 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 -->
|
<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:
|
% 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 -->
|
<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 -->
|
||||||
@@ -632,19 +542,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="container">
|
<td class="container">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<!-- START CENTERED WHITE CONTAINER -->
|
|
||||||
<span class="preheader">Tautulli Newsletter - ${subject}</span>
|
<span class="preheader">Tautulli Newsletter - ${subject}</span>
|
||||||
|
|
||||||
% if base_url and not preview:
|
% if base_url and not preview:
|
||||||
<div class="view-full"> <!-- IGNORE SAVE -->
|
<div class="view-full"> <!-- IGNORE SAVE -->
|
||||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
<a href="${base_url + uuid}" title="View full newsletter" target="_blank">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||||
</div> <!-- IGNORE SAVE -->
|
</div> <!-- IGNORE SAVE -->
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
<table border="0" cellpadding="3" cellspacing="0" class="main">
|
<table border="0" cellpadding="3" cellspacing="0" class="main">
|
||||||
|
|
||||||
<!-- START MAIN CONTENT AREA -->
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="wrapper">
|
<td class="wrapper">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -654,7 +558,6 @@
|
|||||||
<div class="dates">${parameters['start_date']} - ${parameters['end_date']}</div>
|
<div class="dates">${parameters['start_date']} - ${parameters['end_date']}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
% if message:
|
% if message:
|
||||||
<tr>
|
<tr>
|
||||||
<td class="wrapper">
|
<td class="wrapper">
|
||||||
@@ -663,7 +566,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if recently_added.get('movie'):
|
% if recently_added.get('movie'):
|
||||||
<tr>
|
<tr>
|
||||||
<td class="wrapper">
|
<td class="wrapper">
|
||||||
@@ -679,109 +581,105 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
<tbody>
|
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
||||||
% for movie_a, movie_b in grouper(recently_added['movie'], 2):
|
<tr>
|
||||||
<tr>
|
<td>
|
||||||
<td>
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
<tr>
|
||||||
<tbody>
|
% for movie in (movie_a, movie_b):
|
||||||
<tr>
|
% if movie:
|
||||||
% for movie in (movie_a, movie_b):
|
% if not movie_b:
|
||||||
% if movie:
|
<td align="center" valign="top" class="card-instance pad"></td>
|
||||||
% if not movie_b:
|
% endif
|
||||||
<td align="center" valign="top" class="card-instance pad"></td>
|
<td align="center" valign="top" class="card-instance movie">
|
||||||
% endif
|
<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']});">
|
||||||
<td align="center" valign="top" class="card-instance movie">
|
<tr>
|
||||||
<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']});">
|
<td class="card-poster-container">
|
||||||
<tbody>
|
<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>
|
<tr>
|
||||||
<td class="card-poster-container">
|
<td>
|
||||||
<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']})">
|
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">
|
||||||
<tbody>
|
<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">
|
||||||
<tr>
|
</a>
|
||||||
<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>
|
</td>
|
||||||
<td class="card-info-container">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
</table>
|
||||||
<tbody>
|
</td>
|
||||||
<tr>
|
<td class="card-info-container">
|
||||||
<td class="card-info-title nowrap">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<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>
|
<tr>
|
||||||
</td>
|
<td class="card-info-title nowrap">
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</td>
|
||||||
<td class="card-info-body">
|
</tr>
|
||||||
% if movie['tagline']:
|
<tr>
|
||||||
<p class="nowrap mb5">
|
<td class="card-info-body">
|
||||||
<em>${movie['tagline']}</em>
|
% if movie['tagline']:
|
||||||
</p>
|
<p class="nowrap mb5">
|
||||||
% endif
|
<em>${movie['tagline']}</em>
|
||||||
<p>
|
</p>
|
||||||
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
% endif
|
||||||
</p>
|
<p>
|
||||||
</td>
|
${movie['summary'][:450] + (movie['summary'][450:] and '...')}
|
||||||
</tr>
|
</p>
|
||||||
<tr>
|
</td>
|
||||||
<td class="card-info-footer nowrap">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<tr>
|
||||||
<tbody>
|
<td class="card-info-footer nowrap">
|
||||||
<tr>
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
<td class="badge-container">
|
<tr>
|
||||||
% if movie['year']:
|
<td class="badge-container">
|
||||||
<span class="badge" title="${movie['year']}">${movie['year']}</span>
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
% endif
|
<tr>
|
||||||
% if movie['duration']:
|
% if movie['year']:
|
||||||
<% duration = int(int(movie['duration'])/60000) %>
|
<td class="badge" title="${movie['year']}">${movie['year']}</td>
|
||||||
<span class="badge" title="${duration} mins">${duration} mins</span>
|
% endif
|
||||||
% endif
|
% if movie['duration']:
|
||||||
% if movie['genres']:
|
<% duration = int(int(movie['duration'])/60000) %>
|
||||||
% for genre in movie['genres'][:2]:
|
<td class="badge" title="${duration} mins">${duration} mins</td>
|
||||||
<span class="badge" title="${genre}">${genre}</span>
|
% endif
|
||||||
% endfor
|
% if movie['genres']:
|
||||||
% endif
|
% for genre in movie['genres'][:]:
|
||||||
</td>
|
<td class="badge" title="${genre}">${genre}</td>
|
||||||
% if movie['rating']:
|
% endfor
|
||||||
<% rating = int(round(float(movie['rating']) / 2)) %>
|
% endif
|
||||||
<td class="star-rating-container" title="${int(float(movie['rating'])/0.1)}%" align="right">
|
</tr>
|
||||||
% for _ in range(rating):
|
</table>
|
||||||
<span class="star-rating full">★</span>
|
</td>
|
||||||
% endfor
|
% if movie['rating']:
|
||||||
% for _ in range(5-rating):
|
<% rating = int(round(float(movie['rating']) / 2)) %>
|
||||||
<span class="star-rating empty">☆</span>
|
<td class="star-rating-container" title="${int(float(movie['rating'])/0.1)}%" align="right">
|
||||||
% endfor
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
</td>
|
<tr>
|
||||||
% endif
|
% for _ in range(rating):
|
||||||
</tr>
|
<td class="star-rating full">★</td>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
% for _ in range(5-rating):
|
||||||
</td>
|
<td class="star-rating empty">☆</td>
|
||||||
</tr>
|
% endfor
|
||||||
</tbody>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
% if not movie_b:
|
</table>
|
||||||
<td align="center" valign="top" class="card-instance pad"></td>
|
</td>
|
||||||
% endif
|
% if not movie_b:
|
||||||
% endif
|
<td align="center" valign="top" class="card-instance pad"></td>
|
||||||
% endfor
|
% endif
|
||||||
</tr>
|
% endif
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
% endfor
|
</tr>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -803,142 +701,137 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
<tbody>
|
% for show_a, show_b in grouper(recently_added['show'], 2):
|
||||||
% for show_a, show_b in grouper(recently_added['show'], 2):
|
<tr>
|
||||||
<tr>
|
<td>
|
||||||
<td>
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
<tr>
|
||||||
<tbody>
|
% for show in (show_a, show_b):
|
||||||
<tr>
|
% if show:
|
||||||
% for show in (show_a, show_b):
|
% if not show_b:
|
||||||
% if show:
|
<td align="center" valign="top" class="card-instance pad"></td>
|
||||||
% if not show_b:
|
% endif
|
||||||
<td align="center" valign="top" class="card-instance pad"></td>
|
<%
|
||||||
% endif
|
if show['season_count'] == 1:
|
||||||
<%
|
if show['season'][0]['episode_count'] == 1:
|
||||||
if show['season_count'] == 1:
|
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||||
if show['season'][0]['episode_count'] == 1:
|
else:
|
||||||
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
||||||
else:
|
else:
|
||||||
link_rating_key = show['season'][0]['episode'][0]['parent_rating_key']
|
link_rating_key = show['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']});">
|
||||||
<td align="center" valign="top" class="card-instance show">
|
<tr>
|
||||||
<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']});">
|
<td class="card-poster-container">
|
||||||
<tbody>
|
<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>
|
<tr>
|
||||||
<td class="card-poster-container">
|
<td>
|
||||||
<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']})">
|
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">
|
||||||
<tbody>
|
<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">
|
||||||
<tr>
|
</a>
|
||||||
<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>
|
</td>
|
||||||
<td class="card-info-container">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
</table>
|
||||||
<tbody>
|
</td>
|
||||||
<tr>
|
<td class="card-info-container">
|
||||||
<td class="card-info-title nowrap">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<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>
|
<tr>
|
||||||
</td>
|
<td class="card-info-title nowrap">
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</td>
|
||||||
<td class="card-info-body">
|
</tr>
|
||||||
<p class="nowrap mb5">
|
<tr>
|
||||||
% if show['season_count'] > 1:
|
<td class="card-info-body">
|
||||||
<em>${show['season_count']} seasons /</em>
|
<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
|
% endif
|
||||||
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
% if show['duration']:
|
||||||
<em>${total_show_episodes} episode${'s' if total_show_episodes > 1 else ''}</em>
|
<% duration = int(int(show['duration'])/60000) %>
|
||||||
</p>
|
<td class="badge" title="${duration} mins">${duration} mins</td>
|
||||||
<p class="nowrap mb5">
|
% endif
|
||||||
% for i, season in enumerate(show['season'][:8]):
|
% if show['genres']:
|
||||||
Season ${season['media_index']} ·
|
% for genre in show['genres'][:2]:
|
||||||
% if season['episode_count'] == 1:
|
<td class="badge" title="${genre}">${genre}</td>
|
||||||
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
|
|
||||||
% endfor
|
% 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
|
% endif
|
||||||
</p>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
% if show['rating']:
|
||||||
<td class="card-info-footer nowrap">
|
<% rating = int(round(float(show['rating']) / 2)) %>
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<td class="star-rating-container" title="${int(float(show['rating'])/0.1)}%" align="right">
|
||||||
<tbody>
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="badge-container">
|
% for _ in range(rating):
|
||||||
% if show['year']:
|
<td class="star-rating full">★</td>
|
||||||
<span class="badge" title="${show['year']}">${show['year']}</span>
|
% endfor
|
||||||
% endif
|
% for _ in range(5-rating):
|
||||||
% if show['duration']:
|
<td class="star-rating empty">☆</td>
|
||||||
<% duration = int(int(show['duration'])/60000) %>
|
% endfor
|
||||||
<span class="badge" title="${duration} mins">${duration} mins</span>
|
</tr>
|
||||||
% endif
|
</table>
|
||||||
% if show['genres']:
|
</td>
|
||||||
% for genre in show['genres'][:2]:
|
% endif
|
||||||
<span class="badge" title="${genre}">${genre}</span>
|
</tr>
|
||||||
% 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>
|
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
% if not show_b:
|
</table>
|
||||||
<td align="center" valign="top" class="card-instance pad"></td>
|
</td>
|
||||||
% endif
|
% if not show_b:
|
||||||
% endif
|
<td align="center" valign="top" class="card-instance pad"></td>
|
||||||
% endfor
|
% endif
|
||||||
</tr>
|
% endif
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
% endfor
|
</tr>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -960,124 +853,114 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<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):
|
||||||
% for album_a, album_b in grouper([a for artist in recently_added['artist'] for a in artist['album']], 2):
|
<tr>
|
||||||
<tr>
|
<td>
|
||||||
<td>
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
<tr>
|
||||||
<tbody>
|
% for album in (album_a, album_b):
|
||||||
<tr>
|
% if album:
|
||||||
% for album in (album_a, album_b):
|
% if not album_b:
|
||||||
% if album:
|
<td align="center" valign="top" class="card-instance pad"></td>
|
||||||
% if not album_b:
|
% endif
|
||||||
<td align="center" valign="top" class="card-instance pad"></td>
|
<td align="center" valign="top" class="card-instance album">
|
||||||
% endif
|
<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']});">
|
||||||
<td align="center" valign="top" class="card-instance album">
|
<tr>
|
||||||
<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']});">
|
<td class="card-poster-container">
|
||||||
<tbody>
|
<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>
|
<tr>
|
||||||
<td class="card-poster-container">
|
<td>
|
||||||
<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']})">
|
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">
|
||||||
<tbody>
|
<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">
|
||||||
<tr>
|
</a>
|
||||||
<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>
|
</td>
|
||||||
<td class="card-info-container">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
</table>
|
||||||
<tbody>
|
</td>
|
||||||
<tr>
|
<td class="card-info-container">
|
||||||
<td class="card-info-title nowrap">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<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>
|
<tr>
|
||||||
</td>
|
<td class="card-info-title nowrap">
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</td>
|
||||||
<td class="card-info-body">
|
</tr>
|
||||||
<p class="nowrap mb5">
|
<tr>
|
||||||
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
<td class="card-info-body">
|
||||||
</p>
|
<p class="nowrap mb5">
|
||||||
% if artist['title'].lower() != 'various artists':
|
<em>${album['parent_title']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
||||||
<p>
|
</p>
|
||||||
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
% if album['parent_title'].lower() != 'various artists':
|
||||||
</p>
|
<p>
|
||||||
% endif
|
${album['summary'][:200] + (album['summary'][200:] and '...')}
|
||||||
</td>
|
</p>
|
||||||
</tr>
|
% endif
|
||||||
<tr>
|
</td>
|
||||||
<td class="card-info-footer nowrap">
|
</tr>
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<tr>
|
||||||
<tbody>
|
<td class="card-info-footer nowrap">
|
||||||
<tr>
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
<td class="badge-container">
|
<tr>
|
||||||
% if album['year']:
|
<td class="badge-container">
|
||||||
<span class="badge" title="${album['year']}">${album['year']}</span>
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
% endif
|
<tr>
|
||||||
% if album['genres']:
|
% if album['year']:
|
||||||
% for genre in album['genres'][:2]:
|
<td class="badge" title="${album['year']}">${album['year']}</td>
|
||||||
<span class="badge" title="${genre}">${genre}</span>
|
% endif
|
||||||
% endfor
|
% if album['genres']:
|
||||||
% endif
|
% for genre in album['genres'][:2]:
|
||||||
</td>
|
<td class="badge" title="${genre}">${genre}</td>
|
||||||
% if album['rating']:
|
% endfor
|
||||||
<% rating = int(round(float(album['rating']) / 2)) %>
|
% endif
|
||||||
<td class="star-rating-container" title="${int(float(album['rating'])/0.1)}%" align="right">
|
</tr>
|
||||||
% for _ in range(rating):
|
</table>
|
||||||
<span class="star-rating full">★</span>
|
</td>
|
||||||
% endfor
|
% if album['rating']:
|
||||||
% for _ in range(5-rating):
|
<% rating = int(round(float(album['rating']) / 2)) %>
|
||||||
<span class="star-rating empty">☆</span>
|
<td class="star-rating-container" title="${int(float(album['rating'])/0.1)}%" align="right">
|
||||||
% endfor
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
</td>
|
<tr>
|
||||||
% endif
|
% for _ in range(rating):
|
||||||
</tr>
|
<td class="star-rating full">★</td>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
% for _ in range(5-rating):
|
||||||
</td>
|
<td class="star-rating empty">☆</td>
|
||||||
</tr>
|
% endfor
|
||||||
</tbody>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
% if not album_b:
|
</table>
|
||||||
<td align="center" valign="top" class="card-instance pad"></td>
|
</td>
|
||||||
% endif
|
% if not album_b:
|
||||||
% endif
|
<td align="center" valign="top" class="card-instance pad"></td>
|
||||||
% endfor
|
% endif
|
||||||
</tr>
|
% endif
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</td>
|
||||||
% endfor
|
</tr>
|
||||||
</tbody>
|
% endfor
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
<td class="footer">
|
<td class="footer">
|
||||||
<!-- START FOOTER -->
|
|
||||||
<div class="footer-bar"></div>
|
<div class="footer-bar"></div>
|
||||||
<div class="content-block powered-by">
|
<div class="content-block powered-by">
|
||||||
Newsletter generated by <a href="http://tautulli.com" target="_blank">Tautulli</a>.
|
<!-- FOOTER MESSAGE - DO NOT REMOVE -->
|
||||||
</div>
|
</div>
|
||||||
<!-- END FOOTER -->
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- END MAIN CONTENT AREA -->
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- END CENTERED WHITE CONTAINER -->
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
@@ -23,7 +23,7 @@ __author__ = 'The Python-Twitter Developers'
|
|||||||
__email__ = 'python-twitter@googlegroups.com'
|
__email__ = 'python-twitter@googlegroups.com'
|
||||||
__copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers'
|
__copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers'
|
||||||
__license__ = 'Apache License 2.0'
|
__license__ = 'Apache License 2.0'
|
||||||
__version__ = '3.0rc1'
|
__version__ = '3.4.1'
|
||||||
__url__ = 'https://github.com/bear/python-twitter'
|
__url__ = 'https://github.com/bear/python-twitter'
|
||||||
__download_url__ = 'https://pypi.python.org/pypi/python-twitter'
|
__download_url__ = 'https://pypi.python.org/pypi/python-twitter'
|
||||||
__description__ = 'A Python wrapper around the Twitter API'
|
__description__ = 'A Python wrapper around the Twitter API'
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
@@ -47,7 +46,7 @@ class _FileCache(object):
|
|||||||
path = self._GetPath(key)
|
path = self._GetPath(key)
|
||||||
if not path.startswith(self._root_directory):
|
if not path.startswith(self._root_directory):
|
||||||
raise _FileCacheError('%s does not appear to live under %s' %
|
raise _FileCacheError('%s does not appear to live under %s' %
|
||||||
(path, self._root_directory ))
|
(path, self._root_directory))
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
|
|
||||||
@@ -101,61 +100,3 @@ class _FileCache(object):
|
|||||||
|
|
||||||
def _GetPrefix(self, hashed_key):
|
def _GetPrefix(self, hashed_key):
|
||||||
return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])
|
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):
|
def message(self):
|
||||||
'''Returns the first argument used to construct this error.'''
|
'''Returns the first argument used to construct this error.'''
|
||||||
return self.args[0]
|
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):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(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):
|
def AsJsonString(self):
|
||||||
""" Returns the TwitterModel as a JSON string based on key/value
|
""" Returns the TwitterModel as a JSON string based on key/value
|
||||||
pairs returned from the AsDict() method. """
|
pairs returned from the AsDict() method. """
|
||||||
@@ -78,11 +85,14 @@ class TwitterModel(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
json_data = data.copy()
|
||||||
if kwargs:
|
if kwargs:
|
||||||
for key, val in kwargs.items():
|
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):
|
class Media(TwitterModel):
|
||||||
@@ -93,11 +103,14 @@ class Media(TwitterModel):
|
|||||||
self.param_defaults = {
|
self.param_defaults = {
|
||||||
'display_url': None,
|
'display_url': None,
|
||||||
'expanded_url': None,
|
'expanded_url': None,
|
||||||
|
'ext_alt_text': None,
|
||||||
'id': None,
|
'id': None,
|
||||||
'media_url': None,
|
'media_url': None,
|
||||||
'media_url_https': None,
|
'media_url_https': None,
|
||||||
|
'sizes': None,
|
||||||
'type': None,
|
'type': None,
|
||||||
'url': None,
|
'url': None,
|
||||||
|
'video_info': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (param, default) in self.param_defaults.items():
|
for (param, default) in self.param_defaults.items():
|
||||||
@@ -172,8 +185,10 @@ class DirectMessage(TwitterModel):
|
|||||||
self.param_defaults = {
|
self.param_defaults = {
|
||||||
'created_at': None,
|
'created_at': None,
|
||||||
'id': None,
|
'id': None,
|
||||||
|
'recipient': None,
|
||||||
'recipient_id': None,
|
'recipient_id': None,
|
||||||
'recipient_screen_name': None,
|
'recipient_screen_name': None,
|
||||||
|
'sender': None,
|
||||||
'sender_id': None,
|
'sender_id': None,
|
||||||
'sender_screen_name': None,
|
'sender_screen_name': None,
|
||||||
'text': None,
|
'text': None,
|
||||||
@@ -181,6 +196,10 @@ class DirectMessage(TwitterModel):
|
|||||||
|
|
||||||
for (param, default) in self.param_defaults.items():
|
for (param, default) in self.param_defaults.items():
|
||||||
setattr(self, param, kwargs.get(param, default))
|
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):
|
def __repr__(self):
|
||||||
if self.text and len(self.text) > 140:
|
if self.text and len(self.text) > 140:
|
||||||
@@ -206,7 +225,7 @@ class Trend(TwitterModel):
|
|||||||
'query': None,
|
'query': None,
|
||||||
'timestamp': None,
|
'timestamp': None,
|
||||||
'url': None,
|
'url': None,
|
||||||
'volume': None,
|
'tweet_volume': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (param, default) in self.param_defaults.items():
|
for (param, default) in self.param_defaults.items():
|
||||||
@@ -218,6 +237,10 @@ class Trend(TwitterModel):
|
|||||||
self.timestamp,
|
self.timestamp,
|
||||||
self.url)
|
self.url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume(self):
|
||||||
|
return self.tweet_volume
|
||||||
|
|
||||||
|
|
||||||
class Hashtag(TwitterModel):
|
class Hashtag(TwitterModel):
|
||||||
|
|
||||||
@@ -259,12 +282,12 @@ class UserStatus(TwitterModel):
|
|||||||
""" A class representing the UserStatus structure. This is an abbreviated
|
""" A class representing the UserStatus structure. This is an abbreviated
|
||||||
form of the twitter.User object. """
|
form of the twitter.User object. """
|
||||||
|
|
||||||
connections = {'following': False,
|
_connections = {'following': False,
|
||||||
'followed_by': False,
|
'followed_by': False,
|
||||||
'following_received': False,
|
'following_received': False,
|
||||||
'following_requested': False,
|
'following_requested': False,
|
||||||
'blocking': False,
|
'blocking': False,
|
||||||
'muting': False}
|
'muting': False}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.param_defaults = {
|
self.param_defaults = {
|
||||||
@@ -284,10 +307,19 @@ class UserStatus(TwitterModel):
|
|||||||
setattr(self, param, kwargs.get(param, default))
|
setattr(self, param, kwargs.get(param, default))
|
||||||
|
|
||||||
if 'connections' in kwargs:
|
if 'connections' in kwargs:
|
||||||
for param in self.connections:
|
for param in self._connections:
|
||||||
if param in kwargs['connections']:
|
if param in kwargs['connections']:
|
||||||
setattr(self, param, True)
|
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):
|
def __repr__(self):
|
||||||
connections = [param for param in self.connections if getattr(self, param)]
|
connections = [param for param in self.connections if getattr(self, param)]
|
||||||
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
|
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
|
||||||
@@ -307,11 +339,14 @@ class User(TwitterModel):
|
|||||||
'default_profile': None,
|
'default_profile': None,
|
||||||
'default_profile_image': None,
|
'default_profile_image': None,
|
||||||
'description': None,
|
'description': None,
|
||||||
|
'email': None,
|
||||||
'favourites_count': None,
|
'favourites_count': None,
|
||||||
'followers_count': None,
|
'followers_count': None,
|
||||||
|
'following': None,
|
||||||
'friends_count': None,
|
'friends_count': None,
|
||||||
'geo_enabled': None,
|
'geo_enabled': None,
|
||||||
'id': None,
|
'id': None,
|
||||||
|
'id_str': None,
|
||||||
'lang': None,
|
'lang': None,
|
||||||
'listed_count': None,
|
'listed_count': None,
|
||||||
'location': None,
|
'location': None,
|
||||||
@@ -319,12 +354,16 @@ class User(TwitterModel):
|
|||||||
'notifications': None,
|
'notifications': None,
|
||||||
'profile_background_color': None,
|
'profile_background_color': None,
|
||||||
'profile_background_image_url': None,
|
'profile_background_image_url': None,
|
||||||
|
'profile_background_image_url_https': None,
|
||||||
'profile_background_tile': None,
|
'profile_background_tile': None,
|
||||||
'profile_banner_url': None,
|
'profile_banner_url': None,
|
||||||
'profile_image_url': None,
|
'profile_image_url': None,
|
||||||
|
'profile_image_url_https': None,
|
||||||
'profile_link_color': None,
|
'profile_link_color': None,
|
||||||
|
'profile_sidebar_border_color': None,
|
||||||
'profile_sidebar_fill_color': None,
|
'profile_sidebar_fill_color': None,
|
||||||
'profile_text_color': None,
|
'profile_text_color': None,
|
||||||
|
'profile_use_background_image': None,
|
||||||
'protected': None,
|
'protected': None,
|
||||||
'screen_name': None,
|
'screen_name': None,
|
||||||
'status': None,
|
'status': None,
|
||||||
@@ -333,6 +372,8 @@ class User(TwitterModel):
|
|||||||
'url': None,
|
'url': None,
|
||||||
'utc_offset': None,
|
'utc_offset': None,
|
||||||
'verified': None,
|
'verified': None,
|
||||||
|
'withheld_in_countries': None,
|
||||||
|
'withheld_scope': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (param, default) in self.param_defaults.items():
|
for (param, default) in self.param_defaults.items():
|
||||||
@@ -365,6 +406,7 @@ class Status(TwitterModel):
|
|||||||
'current_user_retweet': None,
|
'current_user_retweet': None,
|
||||||
'favorite_count': None,
|
'favorite_count': None,
|
||||||
'favorited': None,
|
'favorited': None,
|
||||||
|
'full_text': None,
|
||||||
'geo': None,
|
'geo': None,
|
||||||
'hashtags': None,
|
'hashtags': None,
|
||||||
'id': None,
|
'id': None,
|
||||||
@@ -377,6 +419,9 @@ class Status(TwitterModel):
|
|||||||
'media': None,
|
'media': None,
|
||||||
'place': None,
|
'place': None,
|
||||||
'possibly_sensitive': None,
|
'possibly_sensitive': None,
|
||||||
|
'quoted_status': None,
|
||||||
|
'quoted_status_id': None,
|
||||||
|
'quoted_status_id_str': None,
|
||||||
'retweet_count': None,
|
'retweet_count': None,
|
||||||
'retweeted': None,
|
'retweeted': None,
|
||||||
'retweeted_status': None,
|
'retweeted_status': None,
|
||||||
@@ -395,6 +440,11 @@ class Status(TwitterModel):
|
|||||||
for (param, default) in self.param_defaults.items():
|
for (param, default) in self.param_defaults.items():
|
||||||
setattr(self, param, kwargs.get(param, default))
|
setattr(self, param, kwargs.get(param, default))
|
||||||
|
|
||||||
|
if kwargs.get('full_text', None):
|
||||||
|
self.tweet_mode = 'extended'
|
||||||
|
else:
|
||||||
|
self.tweet_mode = 'compatibility'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_at_in_seconds(self):
|
def created_at_in_seconds(self):
|
||||||
""" Get the time this status message was posted, in seconds since
|
""" 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
|
string: A string representation of this twitter.Status instance with
|
||||||
the ID of status, username and datetime.
|
the ID of status, username and datetime.
|
||||||
"""
|
"""
|
||||||
|
if self.tweet_mode == 'extended':
|
||||||
|
text = self.full_text
|
||||||
|
else:
|
||||||
|
text = self.text
|
||||||
if self.user:
|
if self.user:
|
||||||
return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format(
|
return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format(
|
||||||
self.id,
|
self.id,
|
||||||
self.user.screen_name,
|
self.user.screen_name,
|
||||||
self.created_at,
|
self.created_at,
|
||||||
self.text)
|
text)
|
||||||
else:
|
else:
|
||||||
return u"Status(ID={0}, Created={1}, Text={2!r})".format(
|
return u"Status(ID={0}, Created={1}, Text={2!r})".format(
|
||||||
self.id,
|
self.id,
|
||||||
self.created_at,
|
self.created_at,
|
||||||
self.text)
|
text)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def NewFromJsonDict(cls, data, **kwargs):
|
def NewFromJsonDict(cls, data, **kwargs):
|
||||||
@@ -439,17 +493,25 @@ class Status(TwitterModel):
|
|||||||
current_user_retweet = None
|
current_user_retweet = None
|
||||||
hashtags = None
|
hashtags = None
|
||||||
media = None
|
media = None
|
||||||
|
quoted_status = None
|
||||||
retweeted_status = None
|
retweeted_status = None
|
||||||
urls = None
|
urls = None
|
||||||
user = None
|
user = None
|
||||||
user_mentions = 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:
|
if 'user' in data:
|
||||||
user = User.NewFromJsonDict(data['user'])
|
user = User.NewFromJsonDict(data['user'])
|
||||||
if 'retweeted_status' in data:
|
if 'retweeted_status' in data:
|
||||||
retweeted_status = Status.NewFromJsonDict(data['retweeted_status'])
|
retweeted_status = Status.NewFromJsonDict(data['retweeted_status'])
|
||||||
if 'current_user_retweet' in data:
|
if 'current_user_retweet' in data:
|
||||||
current_user_retweet = data['current_user_retweet']['id']
|
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 'entities' in data:
|
||||||
if 'urls' in data['entities']:
|
if 'urls' in data['entities']:
|
||||||
@@ -470,6 +532,7 @@ class Status(TwitterModel):
|
|||||||
current_user_retweet=current_user_retweet,
|
current_user_retweet=current_user_retweet,
|
||||||
hashtags=hashtags,
|
hashtags=hashtags,
|
||||||
media=media,
|
media=media,
|
||||||
|
quoted_status=quoted_status,
|
||||||
retweeted_status=retweeted_status,
|
retweeted_status=retweeted_status,
|
||||||
urls=urls,
|
urls=urls,
|
||||||
user=user,
|
user=user,
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class Emoticons:
|
class Emoticons:
|
||||||
POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *",
|
POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *",
|
||||||
":P", ":D", ":d", ":p",
|
":P", ":D", ":d", ":p",
|
||||||
@@ -27,6 +28,7 @@ class Emoticons:
|
|||||||
"[:", ";]"
|
"[:", ";]"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ParseTweet(object):
|
class ParseTweet(object):
|
||||||
# compile once on import
|
# compile once on import
|
||||||
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
|
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
|
||||||
@@ -51,7 +53,7 @@ class ParseTweet(object):
|
|||||||
self.Emoticon = ParseTweet.getAttributeEmoticon(tweet)
|
self.Emoticon = ParseTweet.getAttributeEmoticon(tweet)
|
||||||
|
|
||||||
# additional intelligence
|
# 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]
|
self.Owner = self.UserHandles[0]
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -66,10 +68,10 @@ class ParseTweet(object):
|
|||||||
emoji = list()
|
emoji = list()
|
||||||
for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()):
|
for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()):
|
||||||
if tok in Emoticons.POSITIVE:
|
if tok in Emoticons.POSITIVE:
|
||||||
emoji.append( tok )
|
emoji.append(tok)
|
||||||
continue
|
continue
|
||||||
if tok in Emoticons.NEGATIVE:
|
if tok in Emoticons.NEGATIVE:
|
||||||
emoji.append( tok )
|
emoji.append(tok)
|
||||||
return emoji
|
return emoji
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@@ -97,6 +97,7 @@ class RateLimit(object):
|
|||||||
and a dictionary of limit, remaining, and reset will be returned.
|
and a dictionary of limit, remaining, and reset will be returned.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
self.__dict__['resources'] = {}
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -117,10 +118,12 @@ class RateLimit(object):
|
|||||||
for non_std_endpoint in NON_STANDARD_ENDPOINTS:
|
for non_std_endpoint in NON_STANDARD_ENDPOINTS:
|
||||||
if re.match(non_std_endpoint.regex, resource):
|
if re.match(non_std_endpoint.regex, resource):
|
||||||
return non_std_endpoint.resource
|
return non_std_endpoint.resource
|
||||||
else:
|
return resource
|
||||||
return resource
|
|
||||||
|
|
||||||
def set_unknown_limit(self, url, limit, remaining, reset):
|
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
|
""" If a resource family is unknown, add it to the object's
|
||||||
dictionary. This is to deal with new endpoints being added to
|
dictionary. This is to deal with new endpoints being added to
|
||||||
the API, but not necessarily to the information returned by
|
the API, but not necessarily to the information returned by
|
||||||
@@ -146,13 +149,18 @@ class RateLimit(object):
|
|||||||
"""
|
"""
|
||||||
endpoint = self.url_to_resource(url)
|
endpoint = self.url_to_resource(url)
|
||||||
resource_family = endpoint.split('/')[1]
|
resource_family = endpoint.split('/')[1]
|
||||||
self.__dict__['resources'].update(
|
new_endpoint = {endpoint: {
|
||||||
{resource_family: {
|
"limit": enf_type('limit', int, limit),
|
||||||
endpoint: {
|
"remaining": enf_type('remaining', int, remaining),
|
||||||
"limit": limit,
|
"reset": enf_type('reset', int, reset)
|
||||||
"remaining": remaining,
|
}}
|
||||||
"reset": 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):
|
def get_limit(self, url):
|
||||||
""" Gets a EndpointRateLimit object for the given url.
|
""" Gets a EndpointRateLimit object for the given url.
|
||||||
@@ -181,35 +189,3 @@ class RateLimit(object):
|
|||||||
return EndpointRateLimit(family_rates['limit'],
|
return EndpointRateLimit(family_rates['limit'],
|
||||||
family_rates['remaining'],
|
family_rates['remaining'],
|
||||||
family_rates['reset'])
|
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
|
# encoding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
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
|
import requests
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
|
|
||||||
from twitter import TwitterError
|
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 = [
|
TLDS = [
|
||||||
"ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar",
|
"ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar",
|
||||||
@@ -138,7 +158,14 @@ TLDS = [
|
|||||||
"淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团",
|
"淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团",
|
||||||
"飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"]
|
"飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "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):
|
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.
|
Expected length of the status message as an integer.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
replaced_chars = 0
|
status_length = 0
|
||||||
status_length = len(status)
|
if isinstance(status, bytes):
|
||||||
match = re.findall(URL_REGEXP, status)
|
status = unicode(status)
|
||||||
if len(match) >= 1:
|
for word in re.split(r'\s', status):
|
||||||
replaced_chars = len(''.join(match))
|
if is_url(word):
|
||||||
status_length = status_length - replaced_chars + (short_url_length * len(match))
|
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
|
return status_length
|
||||||
|
|
||||||
|
|
||||||
@@ -171,16 +205,14 @@ def is_url(text):
|
|||||||
Returns:
|
Returns:
|
||||||
Boolean of whether the text should be treated as a URL or not.
|
Boolean of whether the text should be treated as a URL or not.
|
||||||
"""
|
"""
|
||||||
if re.findall(URL_REGEXP, text):
|
return bool(re.findall(URL_REGEXP, text))
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def http_to_file(http):
|
def http_to_file(http):
|
||||||
data_file = NamedTemporaryFile()
|
data_file = NamedTemporaryFile()
|
||||||
req = requests.get(http, stream=True)
|
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
|
return data_file
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +232,8 @@ def parse_media_file(passed_media):
|
|||||||
'image/gif',
|
'image/gif',
|
||||||
'image/bmp',
|
'image/bmp',
|
||||||
'image/webp']
|
'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,
|
# 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
|
# 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 not hasattr(passed_media, 'read'):
|
||||||
if passed_media.startswith('http'):
|
if passed_media.startswith('http'):
|
||||||
data_file = http_to_file(passed_media)
|
data_file = http_to_file(passed_media)
|
||||||
filename = os.path.basename(passed_media)
|
filename = os.path.basename(urlparse(passed_media).path)
|
||||||
else:
|
else:
|
||||||
data_file = open(os.path.realpath(passed_media), 'rb')
|
data_file = open(os.path.realpath(passed_media), 'rb')
|
||||||
filename = os.path.basename(passed_media)
|
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,
|
# Otherwise, if a file object was passed in the first place,
|
||||||
# create the standard reference to media_file (i.e., rename it to fp).
|
# create the standard reference to media_file (i.e., rename it to fp).
|
||||||
else:
|
else:
|
||||||
if passed_media.mode != 'rb':
|
if passed_media.mode not in ['rb', 'rb+', 'w+b']:
|
||||||
raise TwitterError({'message': 'File mode must be "rb".'})
|
raise TwitterError('File mode must be "rb" or "rb+"')
|
||||||
filename = os.path.basename(passed_media.name)
|
filename = os.path.basename(passed_media.name)
|
||||||
data_file = passed_media
|
data_file = passed_media
|
||||||
|
|
||||||
@@ -226,16 +259,17 @@ def parse_media_file(passed_media):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data_file.seek(0)
|
data_file.seek(0)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
media_type = mimetypes.guess_type(os.path.basename(filename))[0]
|
media_type = mimetypes.guess_type(os.path.basename(filename))[0]
|
||||||
if media_type in img_formats and file_size > 5 * 1048576:
|
if media_type is not None:
|
||||||
raise TwitterError({'message': 'Images must be less than 5MB.'})
|
if media_type in img_formats and file_size > 5 * 1048576:
|
||||||
elif media_type in video_formats and file_size > 15 * 1048576:
|
raise TwitterError({'message': 'Images must be less than 5MB.'})
|
||||||
raise TwitterError({'message': 'Videos must be less than 15MB.'})
|
elif media_type in video_formats and file_size > 15 * 1048576:
|
||||||
elif media_type not in img_formats and media_type not in video_formats:
|
raise TwitterError({'message': 'Videos must be less than 15MB.'})
|
||||||
raise TwitterError({'message': 'Media type could not be determined.'})
|
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
|
return data_file, filename, file_size, media_type
|
||||||
|
|
||||||
@@ -263,3 +297,18 @@ def enf_type(field, _type, val):
|
|||||||
raise TwitterError({
|
raise TwitterError({
|
||||||
'message': '"{0}" must be type {1}'.format(field, _type.__name__)
|
'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])
|
||||||
|
@@ -42,6 +42,7 @@ import datafactory
|
|||||||
import libraries
|
import libraries
|
||||||
import logger
|
import logger
|
||||||
import mobile_app
|
import mobile_app
|
||||||
|
import newsletters
|
||||||
import newsletter_handler
|
import newsletter_handler
|
||||||
import notification_handler
|
import notification_handler
|
||||||
import notifiers
|
import notifiers
|
||||||
@@ -152,6 +153,17 @@ def initialize(config_file):
|
|||||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||||
verbose=VERBOSE)
|
verbose=VERBOSE)
|
||||||
|
|
||||||
|
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:
|
if not CONFIG.BACKUP_DIR:
|
||||||
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')
|
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')
|
||||||
if not os.path.exists(CONFIG.BACKUP_DIR):
|
if not os.path.exists(CONFIG.BACKUP_DIR):
|
||||||
@@ -191,6 +203,7 @@ def initialize(config_file):
|
|||||||
logger.error(u"Could not perform upgrades: %s" % e)
|
logger.error(u"Could not perform upgrades: %s" % e)
|
||||||
|
|
||||||
# Add notifier configs to logger blacklist
|
# Add notifier configs to logger blacklist
|
||||||
|
newsletters.blacklist_logger()
|
||||||
notifiers.blacklist_logger()
|
notifiers.blacklist_logger()
|
||||||
mobile_app.blacklist_logger()
|
mobile_app.blacklist_logger()
|
||||||
|
|
||||||
@@ -505,11 +518,12 @@ def dbcheck():
|
|||||||
|
|
||||||
# sessions table :: This is a temp table that logs currently active sessions
|
# sessions table :: This is a temp table that logs currently active sessions
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, '
|
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, session_id TEXT, '
|
||||||
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
|
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
|
||||||
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
|
'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, '
|
'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, '
|
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, year INTEGER, '
|
||||||
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||||
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
|
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
|
||||||
@@ -529,6 +543,7 @@ def dbcheck():
|
|||||||
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
|
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
|
||||||
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
|
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
|
||||||
'synced_version INTEGER, synced_version_profile 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, '
|
'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)'
|
'write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)'
|
||||||
)
|
)
|
||||||
@@ -569,8 +584,9 @@ def dbcheck():
|
|||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS session_history_metadata (id INTEGER PRIMARY KEY, '
|
'CREATE TABLE IF NOT EXISTS session_history_metadata (id INTEGER PRIMARY KEY, '
|
||||||
'rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
'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, '
|
'title TEXT, parent_title TEXT, grandparent_title TEXT, original_title TEXT, full_title TEXT, '
|
||||||
'parent_media_index INTEGER, section_id INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb 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, '
|
'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, '
|
'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, '
|
'duration INTEGER DEFAULT 0, guid TEXT, directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT, '
|
||||||
@@ -1053,6 +1069,36 @@ def dbcheck():
|
|||||||
'ALTER TABLE sessions ADD COLUMN watched INTEGER DEFAULT 0'
|
'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 session_id FROM sessions')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table sessions.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE sessions ADD COLUMN session_id 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
|
# Upgrade session_history table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT reference_id FROM session_history')
|
c_db.execute('SELECT reference_id FROM session_history')
|
||||||
@@ -1139,6 +1185,15 @@ def dbcheck():
|
|||||||
'ALTER TABLE session_history_metadata ADD COLUMN labels TEXT'
|
'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
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT transcode_decision FROM session_history_media_info')
|
c_db.execute('SELECT transcode_decision FROM session_history_media_info')
|
||||||
@@ -1712,8 +1767,8 @@ def dbcheck():
|
|||||||
for row in result:
|
for row in result:
|
||||||
img_hash = notification_handler.set_hash_image_info(
|
img_hash = notification_handler.set_hash_image_info(
|
||||||
rating_key=row['rating_key'], width=1000, height=1500, fallback='poster')
|
rating_key=row['rating_key'], width=1000, height=1500, fallback='poster')
|
||||||
data_factory.set_img_info(img_hash=img_hash, imgur_title=row['poster_title'],
|
data_factory.set_img_info(img_hash=img_hash, img_title=row['poster_title'],
|
||||||
imgur_url=row['poster_url'], delete_hash=row['delete_hash'],
|
img_url=row['poster_url'], delete_hash=row['delete_hash'],
|
||||||
service='imgur')
|
service='imgur')
|
||||||
|
|
||||||
db.action('DROP TABLE poster_urls')
|
db.action('DROP TABLE poster_urls')
|
||||||
@@ -1812,7 +1867,8 @@ def initialize_tracker():
|
|||||||
'appVersion': common.RELEASE,
|
'appVersion': common.RELEASE,
|
||||||
'appId': plexpy.INSTALL_TYPE,
|
'appId': plexpy.INSTALL_TYPE,
|
||||||
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
|
'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,
|
'userLanguage': plexpy.SYS_LANGUAGE,
|
||||||
'documentEncoding': plexpy.SYS_ENCODING,
|
'documentEncoding': plexpy.SYS_ENCODING,
|
||||||
'noninteractive': True
|
'noninteractive': True
|
||||||
|
@@ -226,7 +226,11 @@ class ActivityHandler(object):
|
|||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
this_state = self.timeline['state']
|
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 we already have this session in the temp table, check for state changes
|
||||||
if db_session:
|
if db_session:
|
||||||
@@ -235,10 +239,11 @@ class ActivityHandler(object):
|
|||||||
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
||||||
|
|
||||||
last_state = db_session['state']
|
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
|
# 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
|
# Update the session state and viewOffset
|
||||||
if this_state == 'playing':
|
if this_state == 'playing':
|
||||||
# Update the session in our temp session table
|
# Update the session in our temp session table
|
||||||
|
@@ -15,18 +15,13 @@
|
|||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import json
|
import json
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import re
|
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import database
|
import database
|
||||||
import datafactory
|
import helpers
|
||||||
import libraries
|
import libraries
|
||||||
import log_reader
|
|
||||||
import logger
|
import logger
|
||||||
import notification_handler
|
|
||||||
import notifiers
|
|
||||||
import pmsconnect
|
import pmsconnect
|
||||||
import users
|
import users
|
||||||
|
|
||||||
@@ -39,6 +34,7 @@ class ActivityProcessor(object):
|
|||||||
def write_session(self, session=None, notify=True):
|
def write_session(self, session=None, notify=True):
|
||||||
if session:
|
if session:
|
||||||
values = {'session_key': session.get('session_key', ''),
|
values = {'session_key': session.get('session_key', ''),
|
||||||
|
'session_id': session.get('session_id', ''),
|
||||||
'transcode_key': session.get('transcode_key', ''),
|
'transcode_key': session.get('transcode_key', ''),
|
||||||
'section_id': session.get('section_id', ''),
|
'section_id': session.get('section_id', ''),
|
||||||
'rating_key': session.get('rating_key', ''),
|
'rating_key': session.get('rating_key', ''),
|
||||||
@@ -50,6 +46,7 @@ class ActivityProcessor(object):
|
|||||||
'title': session.get('title', ''),
|
'title': session.get('title', ''),
|
||||||
'parent_title': session.get('parent_title', ''),
|
'parent_title': session.get('parent_title', ''),
|
||||||
'grandparent_title': session.get('grandparent_title', ''),
|
'grandparent_title': session.get('grandparent_title', ''),
|
||||||
|
'original_title': session.get('original_title', ''),
|
||||||
'full_title': session.get('full_title', ''),
|
'full_title': session.get('full_title', ''),
|
||||||
'media_index': session.get('media_index', ''),
|
'media_index': session.get('media_index', ''),
|
||||||
'parent_media_index': session.get('parent_media_index', ''),
|
'parent_media_index': session.get('parent_media_index', ''),
|
||||||
@@ -60,6 +57,7 @@ class ActivityProcessor(object):
|
|||||||
'friendly_name': session.get('friendly_name', ''),
|
'friendly_name': session.get('friendly_name', ''),
|
||||||
'ip_address': session.get('ip_address', ''),
|
'ip_address': session.get('ip_address', ''),
|
||||||
'player': session.get('player', ''),
|
'player': session.get('player', ''),
|
||||||
|
'product': session.get('product', ''),
|
||||||
'platform': session.get('platform', ''),
|
'platform': session.get('platform', ''),
|
||||||
'parent_rating_key': session.get('parent_rating_key', ''),
|
'parent_rating_key': session.get('parent_rating_key', ''),
|
||||||
'grandparent_rating_key': session.get('grandparent_rating_key', ''),
|
'grandparent_rating_key': session.get('grandparent_rating_key', ''),
|
||||||
@@ -114,7 +112,9 @@ class ActivityProcessor(object):
|
|||||||
'stream_audio_channels': session.get('stream_audio_channels', ''),
|
'stream_audio_channels': session.get('stream_audio_channels', ''),
|
||||||
'stream_subtitle_decision': session.get('stream_subtitle_decision', ''),
|
'stream_subtitle_decision': session.get('stream_subtitle_decision', ''),
|
||||||
'stream_subtitle_codec': session.get('stream_subtitle_codec', ''),
|
'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),
|
'raw_stream_info': json.dumps(session),
|
||||||
'stopped': int(time.time())
|
'stopped': int(time.time())
|
||||||
}
|
}
|
||||||
@@ -180,8 +180,9 @@ class ActivityProcessor(object):
|
|||||||
if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'):
|
if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'):
|
||||||
logging_enabled = True
|
logging_enabled = True
|
||||||
else:
|
else:
|
||||||
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
logger.debug(u"Tautulli ActivityProcessor :: Session %s ratingKey %s not logged. "
|
||||||
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
u"Does not meet logging criteria. Media type is '%s'" %
|
||||||
|
(session['session_key'], session['rating_key'], session['media_type']))
|
||||||
return session['id']
|
return session['id']
|
||||||
|
|
||||||
if str(session['paused_counter']).isdigit():
|
if str(session['paused_counter']).isdigit():
|
||||||
@@ -193,15 +194,16 @@ class ActivityProcessor(object):
|
|||||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||||
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||||
logging_enabled = False
|
logging_enabled = False
|
||||||
logger.debug(u"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
logger.debug(u"Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
|
||||||
u"seconds, so we're not logging it." %
|
u"which is less than %s seconds, so we're not logging it." %
|
||||||
(session['rating_key'], str(real_play_time), plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
(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 not is_import and session['media_type'] == 'track':
|
||||||
if real_play_time < 15 and session['duration'] >= 30:
|
if real_play_time < 15 and session['duration'] >= 30:
|
||||||
logging_enabled = False
|
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" %
|
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:
|
elif is_import and import_ignore_interval:
|
||||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||||
(real_play_time < int(import_ignore_interval)):
|
(real_play_time < int(import_ignore_interval)):
|
||||||
@@ -266,14 +268,15 @@ class ActivityProcessor(object):
|
|||||||
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Check if we should group the session, select the last two rows from the user
|
# Check if we should group the session, select the last two rows from the user
|
||||||
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history \
|
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history ' \
|
||||||
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
|
'WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 '
|
||||||
|
|
||||||
args = [session['user_id']]
|
args = [session['user_id'], session['rating_key']]
|
||||||
|
|
||||||
result = self.db.select(query=query, args=args)
|
result = self.db.select(query=query, args=args)
|
||||||
|
|
||||||
new_session = prev_session = None
|
new_session = prev_session = None
|
||||||
|
prev_progress_percent = media_watched_percent = 0
|
||||||
# Get the last insert row id
|
# Get the last insert row id
|
||||||
last_id = self.db.last_insert_id()
|
last_id = self.db.last_insert_id()
|
||||||
|
|
||||||
@@ -290,11 +293,23 @@ class ActivityProcessor(object):
|
|||||||
'user_id': result[1]['user_id'],
|
'user_id': result[1]['user_id'],
|
||||||
'reference_id': result[1]['reference_id']}
|
'reference_id': result[1]['reference_id']}
|
||||||
|
|
||||||
|
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||||
|
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
|
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||||
|
}
|
||||||
|
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
|
||||||
|
media_watched_percent = watched_percent.get(session['media_type'], 0)
|
||||||
|
|
||||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
|
||||||
|
# If previous session view offset less than watched percent,
|
||||||
|
# and new session view offset is greater,
|
||||||
|
# then set the reference_id to the previous row,
|
||||||
|
# else set the reference_id to the new id
|
||||||
if prev_session is None and new_session is None:
|
if prev_session is None and new_session is None:
|
||||||
args = [last_id, last_id]
|
args = [last_id, last_id]
|
||||||
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
elif prev_progress_percent < media_watched_percent and \
|
||||||
|
prev_session['view_offset'] <= new_session['view_offset']:
|
||||||
args = [prev_session['reference_id'], new_session['id']]
|
args = [prev_session['reference_id'], new_session['id']]
|
||||||
else:
|
else:
|
||||||
args = [new_session['id'], new_session['id']]
|
args = [new_session['id'], new_session['id']]
|
||||||
@@ -394,6 +409,7 @@ class ActivityProcessor(object):
|
|||||||
'title': session['title'],
|
'title': session['title'],
|
||||||
'parent_title': session['parent_title'],
|
'parent_title': session['parent_title'],
|
||||||
'grandparent_title': session['grandparent_title'],
|
'grandparent_title': session['grandparent_title'],
|
||||||
|
'original_title': session['original_title'],
|
||||||
'full_title': session['full_title'],
|
'full_title': session['full_title'],
|
||||||
'media_index': metadata['media_index'],
|
'media_index': metadata['media_index'],
|
||||||
'parent_media_index': metadata['parent_media_index'],
|
'parent_media_index': metadata['parent_media_index'],
|
||||||
@@ -451,6 +467,16 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_session_by_id(self, session_id=None):
|
||||||
|
if session_id:
|
||||||
|
session = self.db.select_single('SELECT * FROM sessions '
|
||||||
|
'WHERE session_id = ? ',
|
||||||
|
args=[session_id])
|
||||||
|
if session:
|
||||||
|
return session
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def set_session_state(self, session_key=None, state=None, **kwargs):
|
def set_session_state(self, session_key=None, state=None, **kwargs):
|
||||||
if str(session_key).isdigit():
|
if str(session_key).isdigit():
|
||||||
values = {}
|
values = {}
|
||||||
|
@@ -37,6 +37,8 @@ import logger
|
|||||||
import mobile_app
|
import mobile_app
|
||||||
import notification_handler
|
import notification_handler
|
||||||
import notifiers
|
import notifiers
|
||||||
|
import newsletter_handler
|
||||||
|
import newsletters
|
||||||
import users
|
import users
|
||||||
|
|
||||||
|
|
||||||
@@ -443,6 +445,51 @@ class API2:
|
|||||||
|
|
||||||
return
|
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):
|
def _api_make_md(self):
|
||||||
""" Tries to make a API.md to simplify the api docs. """
|
""" 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
|
# if we fail to generate the output fake an error
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc())
|
logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc())
|
||||||
|
cherrypy.response.status = 500
|
||||||
out['message'] = traceback.format_exc()
|
out['message'] = traceback.format_exc()
|
||||||
out['result'] = 'error'
|
out['result'] = 'error'
|
||||||
|
|
||||||
@@ -573,6 +621,7 @@ General optional parameters:
|
|||||||
out = xmltodict.unparse(out, pretty=True)
|
out = xmltodict.unparse(out, pretty=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result')
|
logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result')
|
||||||
|
cherrypy.response.status = 500
|
||||||
try:
|
try:
|
||||||
out['message'] = e
|
out['message'] = e
|
||||||
out['result'] = 'error'
|
out['result'] = 'error'
|
||||||
@@ -613,6 +662,7 @@ General optional parameters:
|
|||||||
result = call(**self._api_kwargs)
|
result = call(**self._api_kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.api_error(u'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, 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:
|
if self._api_debug:
|
||||||
cherrypy.request.show_tracebacks = True
|
cherrypy.request.show_tracebacks = True
|
||||||
# Reraise the exception so the traceback hits the browser
|
# Reraise the exception so the traceback hits the browser
|
||||||
@@ -657,4 +707,7 @@ General optional parameters:
|
|||||||
if ret.get('result'):
|
if ret.get('result'):
|
||||||
self._api_result_type = ret.pop('result', None)
|
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))
|
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
||||||
|
@@ -20,20 +20,23 @@ import version
|
|||||||
|
|
||||||
# Identify Our Application
|
# Identify Our Application
|
||||||
PLATFORM = platform.system()
|
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)
|
||||||
|
PLATFORM_DEVICE_NAME = platform.node()
|
||||||
BRANCH = version.PLEXPY_BRANCH
|
BRANCH = version.PLEXPY_BRANCH
|
||||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
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_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
||||||
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||||
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
||||||
DEFAULT_ART = "interfaces/default/images/art.png"
|
DEFAULT_ART = "interfaces/default/images/art.png"
|
||||||
|
|
||||||
ONLINE_POSTER_THUMB = "http://tautulli.com/images/poster.png"
|
ONLINE_POSTER_THUMB = "https://tautulli.com/images/poster.png"
|
||||||
ONLINE_COVER_THUMB = "http://tautulli.com/images/cover.png"
|
ONLINE_COVER_THUMB = "https://tautulli.com/images/cover.png"
|
||||||
ONLINE_ART = "http://tautulli.com/images/art.png"
|
ONLINE_ART = "https://tautulli.com/images/art.png"
|
||||||
|
|
||||||
MEDIA_TYPE_HEADERS = {
|
MEDIA_TYPE_HEADERS = {
|
||||||
'movie': 'Movies',
|
'movie': 'Movies',
|
||||||
@@ -337,6 +340,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', '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': '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': '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 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 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'},
|
{'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 +404,7 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'},
|
{'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': '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 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', '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': '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'},
|
{'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', 'example': 'e.g. 6, or 6-10'},
|
||||||
@@ -471,6 +476,7 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'},
|
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'},
|
||||||
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
|
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
|
||||||
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
|
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
|
||||||
|
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
|
||||||
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
|
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
|
||||||
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
|
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
|
||||||
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
|
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
|
||||||
@@ -529,6 +535,7 @@ NEWSLETTER_PARAMETERS = [
|
|||||||
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'},
|
{'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', '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 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.'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -54,6 +54,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PMS_TOKEN': (str, 'PMS', ''),
|
'PMS_TOKEN': (str, 'PMS', ''),
|
||||||
'PMS_SSL': (int, 'PMS', 0),
|
'PMS_SSL': (int, 'PMS', 0),
|
||||||
'PMS_URL': (str, 'PMS', ''),
|
'PMS_URL': (str, 'PMS', ''),
|
||||||
|
'PMS_URL_OVERRIDE': (str, 'PMS', ''),
|
||||||
'PMS_URL_MANUAL': (int, 'PMS', 0),
|
'PMS_URL_MANUAL': (int, 'PMS', 0),
|
||||||
'PMS_USE_BIF': (int, 'PMS', 0),
|
'PMS_USE_BIF': (int, 'PMS', 0),
|
||||||
'PMS_UUID': (str, 'PMS', ''),
|
'PMS_UUID': (str, 'PMS', ''),
|
||||||
@@ -312,7 +313,10 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
|
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
|
||||||
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
|
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
|
||||||
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
|
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
|
||||||
|
'NEWSLETTER_AUTH': (int, 'Newsletter', 0),
|
||||||
|
'NEWSLETTER_PASSWORD': (str, 'Newsletter', ''),
|
||||||
'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
|
'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
|
||||||
|
'NEWSLETTER_INLINE_STYLES': (int, 'Newsletter', 1),
|
||||||
'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),
|
'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),
|
||||||
'NEWSLETTER_DIR': (str, 'Newsletter', ''),
|
'NEWSLETTER_DIR': (str, 'Newsletter', ''),
|
||||||
'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0),
|
'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0),
|
||||||
|
@@ -65,7 +65,7 @@ class DataFactory(object):
|
|||||||
columns = [
|
columns = [
|
||||||
'session_history.reference_id',
|
'session_history.reference_id',
|
||||||
'session_history.id',
|
'session_history.id',
|
||||||
'started AS date',
|
'MAX(started) AS date',
|
||||||
'MIN(started) AS started',
|
'MIN(started) AS started',
|
||||||
'MAX(stopped) AS stopped',
|
'MAX(stopped) AS stopped',
|
||||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \
|
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \
|
||||||
@@ -86,6 +86,7 @@ class DataFactory(object):
|
|||||||
'session_history_metadata.title',
|
'session_history_metadata.title',
|
||||||
'session_history_metadata.parent_title',
|
'session_history_metadata.parent_title',
|
||||||
'session_history_metadata.grandparent_title',
|
'session_history_metadata.grandparent_title',
|
||||||
|
'session_history_metadata.original_title',
|
||||||
'session_history_metadata.year',
|
'session_history_metadata.year',
|
||||||
'session_history_metadata.media_index',
|
'session_history_metadata.media_index',
|
||||||
'session_history_metadata.parent_media_index',
|
'session_history_metadata.parent_media_index',
|
||||||
@@ -132,6 +133,7 @@ class DataFactory(object):
|
|||||||
'title',
|
'title',
|
||||||
'parent_title',
|
'parent_title',
|
||||||
'grandparent_title',
|
'grandparent_title',
|
||||||
|
'original_title',
|
||||||
'year',
|
'year',
|
||||||
'media_index',
|
'media_index',
|
||||||
'parent_media_index',
|
'parent_media_index',
|
||||||
@@ -233,6 +235,7 @@ class DataFactory(object):
|
|||||||
'title': item['parent_title'],
|
'title': item['parent_title'],
|
||||||
'parent_title': item['parent_title'],
|
'parent_title': item['parent_title'],
|
||||||
'grandparent_title': item['grandparent_title'],
|
'grandparent_title': item['grandparent_title'],
|
||||||
|
'original_title': item['original_title'],
|
||||||
'year': item['year'],
|
'year': item['year'],
|
||||||
'media_index': item['media_index'],
|
'media_index': item['media_index'],
|
||||||
'parent_media_index': item['parent_media_index'],
|
'parent_media_index': item['parent_media_index'],
|
||||||
@@ -480,7 +483,8 @@ class DataFactory(object):
|
|||||||
elif stat == 'top_music':
|
elif stat == 'top_music':
|
||||||
top_music = []
|
top_music = []
|
||||||
try:
|
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, ' \
|
'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 ' \
|
'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) - ' \
|
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
|
||||||
@@ -492,7 +496,7 @@ class DataFactory(object):
|
|||||||
' >= datetime("now", "-%s days", "localtime") ' \
|
' >= datetime("now", "-%s days", "localtime") ' \
|
||||||
' AND session_history.media_type = "track" ' \
|
' AND session_history.media_type = "track" ' \
|
||||||
' GROUP BY %s) AS t ' \
|
' GROUP BY %s) AS t ' \
|
||||||
'GROUP BY t.grandparent_title ' \
|
'GROUP BY t.original_title, t.grandparent_title ' \
|
||||||
'ORDER BY %s DESC, started DESC ' \
|
'ORDER BY %s DESC, started DESC ' \
|
||||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
@@ -501,7 +505,7 @@ class DataFactory(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
row = {'title': item['grandparent_title'],
|
row = {'title': item['original_title'] or item['grandparent_title'],
|
||||||
'total_plays': item['total_plays'],
|
'total_plays': item['total_plays'],
|
||||||
'total_duration': item['total_duration'],
|
'total_duration': item['total_duration'],
|
||||||
'users_watched': '',
|
'users_watched': '',
|
||||||
@@ -529,7 +533,8 @@ class DataFactory(object):
|
|||||||
elif stat == 'popular_music':
|
elif stat == 'popular_music':
|
||||||
popular_music = []
|
popular_music = []
|
||||||
try:
|
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, ' \
|
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
|
||||||
'COUNT(DISTINCT t.user_id) AS users_watched, ' \
|
'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 ' \
|
'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") ' \
|
' >= datetime("now", "-%s days", "localtime") ' \
|
||||||
' AND session_history.media_type = "track" ' \
|
' AND session_history.media_type = "track" ' \
|
||||||
' GROUP BY %s) AS t ' \
|
' 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 ' \
|
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
|
||||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
@@ -551,7 +556,7 @@ class DataFactory(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
row = {'title': item['grandparent_title'],
|
row = {'title': item['original_title'] or item['grandparent_title'],
|
||||||
'users_watched': item['users_watched'],
|
'users_watched': item['users_watched'],
|
||||||
'rating_key': item['grandparent_rating_key'],
|
'rating_key': item['grandparent_rating_key'],
|
||||||
'last_play': item['last_watch'],
|
'last_play': item['last_watch'],
|
||||||
@@ -888,7 +893,7 @@ class DataFactory(object):
|
|||||||
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
|
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
|
||||||
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
|
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
|
||||||
'transcode_width, transcode_height, ' \
|
'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 ' \
|
'FROM session_history_media_info ' \
|
||||||
'JOIN session_history ON session_history_media_info.id = session_history.id ' \
|
'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 ' \
|
'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, ' \
|
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
|
||||||
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
|
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
|
||||||
'transcode_width, transcode_height, ' \
|
'transcode_width, transcode_height, ' \
|
||||||
'media_type, title, grandparent_title ' \
|
'media_type, title, grandparent_title, original_title ' \
|
||||||
'FROM sessions ' \
|
'FROM sessions ' \
|
||||||
'WHERE session_key = ? %s' % user_cond
|
'WHERE session_key = ? %s' % user_cond
|
||||||
result = monitor_db.select(query, args=[session_key])
|
result = monitor_db.select(query, args=[session_key])
|
||||||
@@ -979,6 +984,7 @@ class DataFactory(object):
|
|||||||
'media_type': item['media_type'],
|
'media_type': item['media_type'],
|
||||||
'title': item['title'],
|
'title': item['title'],
|
||||||
'grandparent_title': item['grandparent_title'],
|
'grandparent_title': item['grandparent_title'],
|
||||||
|
'original_title': item['original_title'],
|
||||||
'current_session': 1 if session_key else 0,
|
'current_session': 1 if session_key else 0,
|
||||||
'pre_tautulli': pre_tautulli
|
'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.rating_key, session_history_metadata.parent_rating_key, ' \
|
||||||
'session_history_metadata.grandparent_rating_key, session_history_metadata.title, ' \
|
'session_history_metadata.grandparent_rating_key, session_history_metadata.title, ' \
|
||||||
'session_history_metadata.parent_title, session_history_metadata.grandparent_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.media_index, session_history_metadata.parent_media_index, ' \
|
||||||
'session_history_metadata.section_id, session_history_metadata.thumb, ' \
|
'session_history_metadata.section_id, session_history_metadata.thumb, ' \
|
||||||
'session_history_metadata.parent_thumb, session_history_metadata.grandparent_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'],
|
'parent_rating_key': item['parent_rating_key'],
|
||||||
'grandparent_rating_key': item['grandparent_rating_key'],
|
'grandparent_rating_key': item['grandparent_rating_key'],
|
||||||
'grandparent_title': item['grandparent_title'],
|
'grandparent_title': item['grandparent_title'],
|
||||||
|
'original_title': item['original_title'],
|
||||||
'parent_media_index': item['parent_media_index'],
|
'parent_media_index': item['parent_media_index'],
|
||||||
'parent_title': item['parent_title'],
|
'parent_title': item['parent_title'],
|
||||||
'media_index': item['media_index'],
|
'media_index': item['media_index'],
|
||||||
@@ -1215,52 +1223,64 @@ class DataFactory(object):
|
|||||||
|
|
||||||
monitor_db.upsert(table, key_dict=keys, value_dict=values)
|
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()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if rating_key:
|
if not delete_all:
|
||||||
service = service or helpers.get_img_service()
|
service = helpers.get_img_service()
|
||||||
|
|
||||||
if service == 'imgur':
|
if not rating_key and not delete_all:
|
||||||
# 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:
|
|
||||||
logger.error(u"Tautulli DataFactory :: Unable to delete hosted images: rating_key not provided.")
|
logger.error(u"Tautulli DataFactory :: Unable to delete hosted images: rating_key not provided.")
|
||||||
return False
|
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):
|
def get_poster_info(self, rating_key='', metadata=None, service=None):
|
||||||
poster_key = ''
|
poster_key = ''
|
||||||
if str(rating_key).isdigit():
|
if str(rating_key).isdigit():
|
||||||
@@ -1447,7 +1467,7 @@ class DataFactory(object):
|
|||||||
result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'),
|
result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'),
|
||||||
args=[item['parent_rating_key']])
|
args=[item['parent_rating_key']])
|
||||||
for item in result:
|
for item in result:
|
||||||
key = item['media_index']
|
key = item['media_index'] if item['media_index'] else item['title']
|
||||||
children.update({key: {'rating_key': item['rating_key']}})
|
children.update({key: {'rating_key': item['rating_key']}})
|
||||||
|
|
||||||
key = item['parent_media_index'] if match_type == 'index' else item['parent_title']
|
key = item['parent_media_index'] if match_type == 'index' else item['parent_title']
|
||||||
@@ -1538,8 +1558,11 @@ class DataFactory(object):
|
|||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
# Create full_title
|
# 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'])
|
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:
|
else:
|
||||||
full_title = metadata['title']
|
full_title = metadata['title']
|
||||||
|
|
||||||
@@ -1554,7 +1577,8 @@ class DataFactory(object):
|
|||||||
|
|
||||||
# Update the session_history_metadata table
|
# Update the session_history_metadata table
|
||||||
query = 'UPDATE session_history_metadata SET rating_key = ?, parent_rating_key = ?, ' \
|
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 = ?, ' \
|
'media_index = ?, parent_media_index = ?, section_id = ?, thumb = ?, parent_thumb = ?, ' \
|
||||||
'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \
|
'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \
|
||||||
'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \
|
'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \
|
||||||
@@ -1563,7 +1587,8 @@ class DataFactory(object):
|
|||||||
'WHERE rating_key = ?'
|
'WHERE rating_key = ?'
|
||||||
|
|
||||||
args = [metadata['rating_key'], metadata['parent_rating_key'], metadata['grandparent_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['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'],
|
||||||
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
|
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
|
||||||
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
||||||
|
@@ -50,7 +50,9 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
'FROM (SELECT * FROM session_history ' \
|
||||||
|
'GROUP BY date(started, "unixepoch", "localtime"), %s) ' \
|
||||||
|
'AS session_history ' \
|
||||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY date_played ' \
|
'GROUP BY date_played ' \
|
||||||
'ORDER BY started ASC' % (group_by, time_range, user_cond)
|
'ORDER BY started ASC' % (group_by, time_range, user_cond)
|
||||||
@@ -147,7 +149,9 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
'FROM (SELECT * FROM session_history ' \
|
||||||
|
'GROUP BY strftime("%%w", datetime(started, "unixepoch", "localtime")), %s) ' \
|
||||||
|
'AS session_history ' \
|
||||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY dayofweek ' \
|
'GROUP BY dayofweek ' \
|
||||||
'ORDER BY daynumber' % (group_by, time_range, user_cond)
|
'ORDER BY daynumber' % (group_by, time_range, user_cond)
|
||||||
@@ -245,7 +249,9 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
'FROM (SELECT * FROM session_history ' \
|
||||||
|
'GROUP BY strftime("%%H", datetime(started, "unixepoch", "localtime")) , %s) ' \
|
||||||
|
'AS session_history ' \
|
||||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY hourofday ' \
|
'GROUP BY hourofday ' \
|
||||||
'ORDER BY hourofday' % (group_by, time_range, user_cond)
|
'ORDER BY hourofday' % (group_by, time_range, user_cond)
|
||||||
@@ -335,7 +341,9 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
'FROM (SELECT * FROM session_history ' \
|
||||||
|
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")), %s) ' \
|
||||||
|
'AS session_history ' \
|
||||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
||||||
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
|
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
|
||||||
'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
|
'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
|
||||||
@@ -591,7 +599,9 @@ class Graphs(object):
|
|||||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||||
'THEN 1 ELSE 0 END) AS tc_count ' \
|
'THEN 1 ELSE 0 END) AS tc_count ' \
|
||||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
'FROM (SELECT * FROM session_history ' \
|
||||||
|
'GROUP BY date(session_history.started, "unixepoch", "localtime"), %s) ' \
|
||||||
|
'AS session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||||
|
@@ -209,6 +209,9 @@ def now():
|
|||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
return now.strftime("%Y-%m-%d %H:%M:%S")
|
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'):
|
def human_duration(s, sig='dhms'):
|
||||||
|
|
||||||
@@ -835,7 +838,10 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
|
|||||||
)
|
)
|
||||||
|
|
||||||
img_options = {'format': img_format,
|
img_options = {'format': img_format,
|
||||||
'version': int(time.time())}
|
'fetch_format': 'auto',
|
||||||
|
'quality': 'auto',
|
||||||
|
'version': int(time.time()),
|
||||||
|
'secure': True}
|
||||||
|
|
||||||
if width != 1000:
|
if width != 1000:
|
||||||
img_options['width'] = str(width)
|
img_options['width'] = str(width)
|
||||||
@@ -947,7 +953,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
|||||||
"""
|
"""
|
||||||
valid_tokens = re.compile(r'(\(|\)|and|or)')
|
valid_tokens = re.compile(r'(\(|\)|and|or)')
|
||||||
conditions_pattern = re.compile(r'{\d+}')
|
conditions_pattern = re.compile(r'{\d+}')
|
||||||
|
|
||||||
tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()]
|
tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()]
|
||||||
|
|
||||||
stack = [[]]
|
stack = [[]]
|
||||||
@@ -958,7 +964,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
|||||||
close_bracket_next = False
|
close_bracket_next = False
|
||||||
nest_and = 0
|
nest_and = 0
|
||||||
nest_nest_and = 0
|
nest_nest_and = 0
|
||||||
|
|
||||||
for i, x in enumerate(tokens):
|
for i, x in enumerate(tokens):
|
||||||
if open_bracket_next and x == '(':
|
if open_bracket_next and x == '(':
|
||||||
stack[-1].append([])
|
stack[-1].append([])
|
||||||
@@ -969,7 +975,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
|||||||
close_bracket_next = False
|
close_bracket_next = False
|
||||||
if nest_and:
|
if nest_and:
|
||||||
nest_nest_and += 1
|
nest_nest_and += 1
|
||||||
|
|
||||||
elif close_bracket_next and x == ')':
|
elif close_bracket_next and x == ')':
|
||||||
stack.pop()
|
stack.pop()
|
||||||
if not stack:
|
if not stack:
|
||||||
@@ -998,7 +1004,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
|||||||
if nest_and > nest_nest_and:
|
if nest_and > nest_nest_and:
|
||||||
stack.pop()
|
stack.pop()
|
||||||
nest_and -= 1
|
nest_and -= 1
|
||||||
|
|
||||||
elif bool_next and x == 'and' and i < len(tokens)-1:
|
elif bool_next and x == 'and' and i < len(tokens)-1:
|
||||||
stack[-1].append([])
|
stack[-1].append([])
|
||||||
stack.append(stack[-1][-1])
|
stack.append(stack[-1][-1])
|
||||||
@@ -1009,7 +1015,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
|||||||
open_bracket_next = True
|
open_bracket_next = True
|
||||||
close_bracket_next = False
|
close_bracket_next = False
|
||||||
nest_and += 1
|
nest_and += 1
|
||||||
|
|
||||||
elif bool_next and x == 'or' and i < len(tokens)-1:
|
elif bool_next and x == 'or' and i < len(tokens)-1:
|
||||||
stack[-1].append(x)
|
stack[-1].append(x)
|
||||||
cond_next = True
|
cond_next = True
|
||||||
|
@@ -39,12 +39,13 @@ class HTTPHandler(object):
|
|||||||
else:
|
else:
|
||||||
self.urls = urls
|
self.urls = urls
|
||||||
|
|
||||||
self.headers = {'X-Plex-Device-Name': 'Tautulli',
|
self.headers = {'X-Plex-Product': 'Tautulli',
|
||||||
'X-Plex-Product': 'Tautulli',
|
|
||||||
'X-Plex-Version': plexpy.common.RELEASE,
|
'X-Plex-Version': plexpy.common.RELEASE,
|
||||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
|
||||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
|
||||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||||
|
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||||
|
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
||||||
|
'X-Plex-Device': 'Web',
|
||||||
|
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
self.token = token
|
self.token = token
|
||||||
@@ -178,5 +179,5 @@ class HTTPHandler(object):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.response_type, e))
|
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.output_format, e))
|
||||||
return None
|
return None
|
||||||
|
@@ -862,13 +862,13 @@ class Libraries(object):
|
|||||||
if str(section_id).isdigit():
|
if str(section_id).isdigit():
|
||||||
query = 'SELECT session_history.id, session_history.media_type, ' \
|
query = 'SELECT session_history.id, session_history.media_type, ' \
|
||||||
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
|
'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 ' \
|
'year, started, user, content_rating, labels, section_id ' \
|
||||||
'FROM session_history_metadata ' \
|
'FROM session_history_metadata ' \
|
||||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||||
'WHERE section_id = ? ' \
|
'WHERE section_id = ? ' \
|
||||||
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
|
'GROUP BY session_history.rating_key ' \
|
||||||
' ELSE session_history.rating_key END) ' \
|
|
||||||
'ORDER BY started DESC LIMIT ?'
|
'ORDER BY started DESC LIMIT ?'
|
||||||
result = monitor_db.select(query, args=[section_id, limit])
|
result = monitor_db.select(query, args=[section_id, limit])
|
||||||
else:
|
else:
|
||||||
@@ -893,6 +893,7 @@ class Libraries(object):
|
|||||||
'title': row['title'],
|
'title': row['title'],
|
||||||
'parent_title': row['parent_title'],
|
'parent_title': row['parent_title'],
|
||||||
'grandparent_title': row['grandparent_title'],
|
'grandparent_title': row['grandparent_title'],
|
||||||
|
'original_title': row['original_title'],
|
||||||
'thumb': thumb,
|
'thumb': thumb,
|
||||||
'media_index': row['media_index'],
|
'media_index': row['media_index'],
|
||||||
'parent_media_index': row['parent_media_index'],
|
'parent_media_index': row['parent_media_index'],
|
||||||
|
@@ -28,6 +28,7 @@ import traceback
|
|||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import helpers
|
import helpers
|
||||||
|
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||||
|
|
||||||
# These settings are for file logging only
|
# These settings are for file logging only
|
||||||
FILENAME = "tautulli.log"
|
FILENAME = "tautulli.log"
|
||||||
@@ -48,6 +49,20 @@ logger_plex_websocket = logging.getLogger("plex_websocket")
|
|||||||
# Global queue for multiprocessing logging
|
# Global queue for multiprocessing logging
|
||||||
queue = None
|
queue = None
|
||||||
|
|
||||||
|
|
||||||
|
def blacklist_config(config):
|
||||||
|
blacklist = set()
|
||||||
|
blacklist_keys = ['HOOK', 'APIKEY', 'KEY', 'PASSWORD', 'TOKEN']
|
||||||
|
|
||||||
|
for key, value in config.iteritems():
|
||||||
|
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
||||||
|
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
|
||||||
|
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
|
||||||
|
blacklist.add(value.strip())
|
||||||
|
|
||||||
|
_BLACKLIST_WORDS.update(blacklist)
|
||||||
|
|
||||||
|
|
||||||
class NoThreadFilter(logging.Filter):
|
class NoThreadFilter(logging.Filter):
|
||||||
"""
|
"""
|
||||||
Log filter for the current thread
|
Log filter for the current thread
|
||||||
|
@@ -138,7 +138,5 @@ def set_last_seen(device_token=None):
|
|||||||
|
|
||||||
def blacklist_logger():
|
def blacklist_logger():
|
||||||
devices = get_mobile_devices()
|
devices = get_mobile_devices()
|
||||||
|
for d in devices:
|
||||||
blacklist = set(d['device_token'] for d in devices)
|
logger.blacklist_config(d)
|
||||||
|
|
||||||
logger._BLACKLIST_WORDS.update(blacklist)
|
|
||||||
|
@@ -19,6 +19,7 @@ from itertools import groupby
|
|||||||
from mako.lookup import TemplateLookup
|
from mako.lookup import TemplateLookup
|
||||||
from mako import exceptions
|
from mako import exceptions
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
import common
|
||||||
@@ -195,6 +196,7 @@ def add_newsletter_config(agent_id=None, **kwargs):
|
|||||||
newsletter_id = db.last_insert_id()
|
newsletter_id = db.last_insert_id()
|
||||||
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
|
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
|
||||||
% (agent['label'], newsletter_id))
|
% (agent['label'], newsletter_id))
|
||||||
|
blacklist_logger()
|
||||||
return newsletter_id
|
return newsletter_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
|
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
|
||||||
@@ -205,7 +207,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
|||||||
if str(agent_id).isdigit():
|
if str(agent_id).isdigit():
|
||||||
agent_id = int(agent_id)
|
agent_id = int(agent_id)
|
||||||
else:
|
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)
|
% agent_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -253,6 +255,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
|||||||
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
|
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
|
||||||
% (agent['label'], newsletter_id))
|
% (agent['label'], newsletter_id))
|
||||||
newsletter_handler.schedule_newsletters(newsletter_id=newsletter_id)
|
newsletter_handler.schedule_newsletters(newsletter_id=newsletter_id)
|
||||||
|
blacklist_logger()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
|
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
|
||||||
@@ -273,6 +276,17 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
|
|||||||
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
|
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
|
||||||
|
|
||||||
|
|
||||||
|
def blacklist_logger():
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
notifiers = db.select('SELECT newsletter_config, email_config FROM newsletters')
|
||||||
|
|
||||||
|
for n in notifiers:
|
||||||
|
config = json.loads(n['newsletter_config'] or '{}')
|
||||||
|
logger.blacklist_config(config)
|
||||||
|
email_config = json.loads(n['email_config'] or '{}')
|
||||||
|
logger.blacklist_config(email_config)
|
||||||
|
|
||||||
|
|
||||||
def serve_template(templatename, **kwargs):
|
def serve_template(templatename, **kwargs):
|
||||||
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
|
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
|
||||||
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
||||||
@@ -280,13 +294,16 @@ def serve_template(templatename, **kwargs):
|
|||||||
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
||||||
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES)
|
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'])
|
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
template = _hplookup.get_template(templatename)
|
template = _hplookup.get_template(templatename)
|
||||||
return template.render(**kwargs)
|
return template.render(**kwargs), False
|
||||||
except:
|
except:
|
||||||
return exceptions.html_error_template().render()
|
return exceptions.html_error_template().render(), True
|
||||||
|
|
||||||
|
|
||||||
def generate_newsletter_uuid():
|
def generate_newsletter_uuid():
|
||||||
@@ -319,7 +336,6 @@ class Newsletter(object):
|
|||||||
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
|
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
|
||||||
_DEFAULT_MESSAGE = ''
|
_DEFAULT_MESSAGE = ''
|
||||||
_DEFAULT_FILENAME = 'newsletter_{newsletter_uuid}.html'
|
_DEFAULT_FILENAME = 'newsletter_{newsletter_uuid}.html'
|
||||||
_TEMPLATE_MASTER = ''
|
|
||||||
_TEMPLATE = ''
|
_TEMPLATE = ''
|
||||||
|
|
||||||
def __init__(self, newsletter_id=None, newsletter_id_name=None, config=None, email_config=None,
|
def __init__(self, newsletter_id=None, newsletter_id_name=None, config=None, email_config=None,
|
||||||
@@ -373,6 +389,7 @@ class Newsletter(object):
|
|||||||
self.newsletter = None
|
self.newsletter = None
|
||||||
|
|
||||||
self.is_preview = False
|
self.is_preview = False
|
||||||
|
self.template_error = None
|
||||||
|
|
||||||
def set_config(self, config=None, default=None):
|
def set_config(self, config=None, default=None):
|
||||||
return self._validate_config(config=config, default=default)
|
return self._validate_config(config=config, default=default)
|
||||||
@@ -412,19 +429,14 @@ class Newsletter(object):
|
|||||||
'parameters': self.parameters,
|
'parameters': self.parameters,
|
||||||
'data': self.data}
|
'data': self.data}
|
||||||
|
|
||||||
def generate_newsletter(self, preview=False, master=False):
|
def generate_newsletter(self, preview=False):
|
||||||
if preview:
|
if preview:
|
||||||
self.is_preview = True
|
self.is_preview = True
|
||||||
|
|
||||||
if master:
|
|
||||||
template = self._TEMPLATE_MASTER
|
|
||||||
else:
|
|
||||||
template = self._TEMPLATE
|
|
||||||
|
|
||||||
self.retrieve_data()
|
self.retrieve_data()
|
||||||
|
|
||||||
return serve_template(
|
newsletter_rendered, self.template_error = serve_template(
|
||||||
templatename=template,
|
templatename=self._TEMPLATE,
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
subject=self.subject_formatted,
|
subject=self.subject_formatted,
|
||||||
body=self.body_formatted,
|
body=self.body_formatted,
|
||||||
@@ -434,9 +446,36 @@ class Newsletter(object):
|
|||||||
preview=self.is_preview
|
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):
|
def send(self):
|
||||||
self.newsletter = self.generate_newsletter()
|
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():
|
if not self._has_data():
|
||||||
logger.warn(u"Tautulli Newsletters :: %s newsletter has no data. Newsletter not sent." % self.NAME)
|
logger.warn(u"Tautulli Newsletters :: %s newsletter has no data. Newsletter not sent." % self.NAME)
|
||||||
return False
|
return False
|
||||||
@@ -462,6 +501,7 @@ class Newsletter(object):
|
|||||||
for line in self.newsletter.encode('utf-8').splitlines():
|
for line in self.newsletter.encode('utf-8').splitlines():
|
||||||
if '<!-- IGNORE SAVE -->' not in line:
|
if '<!-- IGNORE SAVE -->' not in line:
|
||||||
n_file.write(line + '\r\n')
|
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:
|
except OSError as e:
|
||||||
@@ -470,18 +510,26 @@ class Newsletter(object):
|
|||||||
|
|
||||||
def _send(self):
|
def _send(self):
|
||||||
if self.config['formatted']:
|
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']:
|
if self.email_config['notifier_id']:
|
||||||
return send_notification(
|
return send_notification(
|
||||||
notifier_id=self.email_config['notifier_id'],
|
notifier_id=self.email_config['notifier_id'],
|
||||||
subject=self.subject_formatted,
|
subject=self.subject_formatted,
|
||||||
body=self.newsletter
|
body=newsletter_stripped,
|
||||||
|
plaintext=plaintext
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
email = EMAIL(config=self.email_config)
|
email = EMAIL(config=self.email_config)
|
||||||
return email.notify(
|
return email.notify(
|
||||||
subject=self.subject_formatted,
|
subject=self.subject_formatted,
|
||||||
body=self.newsletter
|
body=newsletter_stripped,
|
||||||
|
plaintext=plaintext
|
||||||
)
|
)
|
||||||
elif self.config['notifier_id']:
|
elif self.config['notifier_id']:
|
||||||
return send_notification(
|
return send_notification(
|
||||||
@@ -514,7 +562,8 @@ class Newsletter(object):
|
|||||||
'newsletter_static_url': base_url + 'id/' + self.newsletter_id_name,
|
'newsletter_static_url': base_url + 'id/' + self.newsletter_id_name,
|
||||||
'newsletter_uuid': self.uuid,
|
'newsletter_uuid': self.uuid,
|
||||||
'newsletter_id': self.newsletter_id,
|
'newsletter_id': self.newsletter_id,
|
||||||
'newsletter_id_name': self.newsletter_id_name
|
'newsletter_id_name': self.newsletter_id_name,
|
||||||
|
'newsletter_password': plexpy.CONFIG.NEWSLETTER_PASSWORD
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
@@ -600,7 +649,6 @@ class RecentlyAdded(Newsletter):
|
|||||||
_DEFAULT_SUBJECT = 'Recently Added to {server_name}! ({end_date})'
|
_DEFAULT_SUBJECT = 'Recently Added to {server_name}! ({end_date})'
|
||||||
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
|
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
|
||||||
_DEFAULT_MESSAGE = ''
|
_DEFAULT_MESSAGE = ''
|
||||||
_TEMPLATE_MASTER = 'recently_added_master.html'
|
|
||||||
_TEMPLATE = 'recently_added.html'
|
_TEMPLATE = 'recently_added.html'
|
||||||
|
|
||||||
def _get_recently_added(self, media_type=None):
|
def _get_recently_added(self, media_type=None):
|
||||||
@@ -757,8 +805,9 @@ class RecentlyAdded(Newsletter):
|
|||||||
else:
|
else:
|
||||||
item['art_hash'] = ''
|
item['art_hash'] = ''
|
||||||
|
|
||||||
item['poster_url'] = ''
|
item['thumb_url'] = ''
|
||||||
item['art_url'] = ''
|
item['art_url'] = ''
|
||||||
|
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
|
||||||
|
|
||||||
elif helpers.get_img_service():
|
elif helpers.get_img_service():
|
||||||
# Upload posters and art to image hosting service
|
# Upload posters and art to image hosting service
|
||||||
@@ -774,7 +823,7 @@ class RecentlyAdded(Newsletter):
|
|||||||
img=item['thumb'], rating_key=item['rating_key'], title=item['title'],
|
img=item['thumb'], rating_key=item['rating_key'], title=item['title'],
|
||||||
width=150, height=height, fallback=fallback)
|
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_info = get_img_info(
|
||||||
img=item['art'], rating_key=item['rating_key'], title=item['title'],
|
img=item['art'], rating_key=item['rating_key'], title=item['title'],
|
||||||
@@ -784,6 +833,15 @@ class RecentlyAdded(Newsletter):
|
|||||||
|
|
||||||
item['thumb_hash'] = ''
|
item['thumb_hash'] = ''
|
||||||
item['art_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
|
self.data['recently_added'] = recently_added
|
||||||
|
|
||||||
|
@@ -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'])
|
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
|
||||||
return stream_data['machine_id'] not in user_devices
|
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'])
|
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
||||||
|
|
||||||
if notify_action == 'on_stop':
|
if notify_action == 'on_stop':
|
||||||
@@ -326,7 +326,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
|
|
||||||
|
|
||||||
def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
|
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)
|
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||||
|
|
||||||
@@ -633,6 +633,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['parent_title'])
|
notify_params['parent_title'])
|
||||||
else:
|
else:
|
||||||
poster_thumb = ''
|
poster_thumb = ''
|
||||||
|
poster_key = ''
|
||||||
|
poster_title = ''
|
||||||
|
|
||||||
img_service = helpers.get_img_service(include_self=True)
|
img_service = helpers.get_img_service(include_self=True)
|
||||||
if img_service not in (None, 'self-hosted'):
|
if img_service not in (None, 'self-hosted'):
|
||||||
@@ -742,6 +744,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
'optimized_version': notify_params['optimized_version'],
|
'optimized_version': notify_params['optimized_version'],
|
||||||
'optimized_version_profile': notify_params['optimized_version_profile'],
|
'optimized_version_profile': notify_params['optimized_version_profile'],
|
||||||
'synced_version': notify_params['synced_version'],
|
'synced_version': notify_params['synced_version'],
|
||||||
|
'live': notify_params['live'],
|
||||||
'stream_local': notify_params['local'],
|
'stream_local': notify_params['local'],
|
||||||
'stream_location': notify_params['location'],
|
'stream_location': notify_params['location'],
|
||||||
'stream_bandwidth': notify_params['bandwidth'],
|
'stream_bandwidth': notify_params['bandwidth'],
|
||||||
@@ -802,6 +805,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
'artist_name': artist_name,
|
'artist_name': artist_name,
|
||||||
'album_name': album_name,
|
'album_name': album_name,
|
||||||
'track_name': track_name,
|
'track_name': track_name,
|
||||||
|
'track_artist': notify_params['original_title'] or notify_params['grandparent_title'],
|
||||||
'season_num': season_num,
|
'season_num': season_num,
|
||||||
'season_num00': season_num00,
|
'season_num00': season_num00,
|
||||||
'episode_num': episode_num,
|
'episode_num': episode_num,
|
||||||
@@ -879,6 +883,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
'subtitle_language': notify_params['subtitle_language'],
|
'subtitle_language': notify_params['subtitle_language'],
|
||||||
'subtitle_language_code': notify_params['subtitle_language_code'],
|
'subtitle_language_code': notify_params['subtitle_language_code'],
|
||||||
'file': notify_params['file'],
|
'file': notify_params['file'],
|
||||||
|
'filename': os.path.basename(notify_params['file']),
|
||||||
'file_size': helpers.humanFileSize(notify_params['file_size']),
|
'file_size': helpers.humanFileSize(notify_params['file_size']),
|
||||||
'indexes': notify_params['indexes'],
|
'indexes': notify_params['indexes'],
|
||||||
'section_id': notify_params['section_id'],
|
'section_id': notify_params['section_id'],
|
||||||
|
@@ -62,7 +62,6 @@ import mobile_app
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
import request
|
import request
|
||||||
import users
|
import users
|
||||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
|
||||||
|
|
||||||
|
|
||||||
BROWSER_NOTIFIERS = {}
|
BROWSER_NOTIFIERS = {}
|
||||||
@@ -420,7 +419,7 @@ def get_notifiers(notifier_id=None, notify_action=None):
|
|||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
|
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
|
||||||
% (', '.join(notify_actions), where), args=args)
|
% (', '.join(notify_actions), where), args=args)
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
||||||
|
|
||||||
@@ -509,7 +508,7 @@ def add_notifier_config(agent_id=None, **kwargs):
|
|||||||
'agent_name': agent['name'],
|
'agent_name': agent['name'],
|
||||||
'agent_label': agent['label'],
|
'agent_label': agent['label'],
|
||||||
'friendly_name': '',
|
'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': json.dumps(DEFAULT_CUSTOM_CONDITIONS),
|
||||||
'custom_conditions_logic': ''
|
'custom_conditions_logic': ''
|
||||||
}
|
}
|
||||||
@@ -540,7 +539,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
|||||||
if str(agent_id).isdigit():
|
if str(agent_id).isdigit():
|
||||||
agent_id = int(agent_id)
|
agent_id = int(agent_id)
|
||||||
else:
|
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)
|
% agent_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -570,7 +569,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
|||||||
'agent_name': agent['name'],
|
'agent_name': agent['name'],
|
||||||
'agent_label': agent['label'],
|
'agent_label': agent['label'],
|
||||||
'friendly_name': kwargs.get('friendly_name', ''),
|
'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': kwargs.get('custom_conditions', json.dumps(DEFAULT_CUSTOM_CONDITIONS)),
|
||||||
'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
|
'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
|
||||||
}
|
}
|
||||||
@@ -612,17 +611,9 @@ def blacklist_logger():
|
|||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
notifiers = db.select('SELECT notifier_config FROM notifiers')
|
notifiers = db.select('SELECT notifier_config FROM notifiers')
|
||||||
|
|
||||||
blacklist = set()
|
|
||||||
blacklist_keys = ['hook', 'key', 'password', 'token']
|
|
||||||
|
|
||||||
for n in notifiers:
|
for n in notifiers:
|
||||||
config = json.loads(n['notifier_config'] or '{}')
|
config = json.loads(n['notifier_config'] or '{}')
|
||||||
for key, value in config.iteritems():
|
logger.blacklist_config(config)
|
||||||
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
|
||||||
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
|
|
||||||
blacklist.add(value.strip())
|
|
||||||
|
|
||||||
logger._BLACKLIST_WORDS.update(blacklist)
|
|
||||||
|
|
||||||
|
|
||||||
class PrettyMetadata(object):
|
class PrettyMetadata(object):
|
||||||
@@ -661,9 +652,9 @@ class PrettyMetadata(object):
|
|||||||
poster_url = self.parameters['poster_url']
|
poster_url = self.parameters['poster_url']
|
||||||
if not poster_url:
|
if not poster_url:
|
||||||
if self.media_type in ('artist', 'album', 'track'):
|
if self.media_type in ('artist', 'album', 'track'):
|
||||||
poster_url = 'http://tautulli.com/images/cover.png'
|
poster_url = common.ONLINE_COVER_THUMB
|
||||||
else:
|
else:
|
||||||
poster_url = 'http://tautulli.com/images/poster.png'
|
poster_url = common.ONLINE_POSTER_THUMB
|
||||||
return poster_url
|
return poster_url
|
||||||
|
|
||||||
def get_provider_name(self, provider):
|
def get_provider_name(self, provider):
|
||||||
@@ -682,13 +673,13 @@ class PrettyMetadata(object):
|
|||||||
provider_name = 'Trakt.tv'
|
provider_name = 'Trakt.tv'
|
||||||
elif provider == 'lastfm':
|
elif provider == 'lastfm':
|
||||||
provider_name = 'Last.fm'
|
provider_name = 'Last.fm'
|
||||||
else:
|
# else:
|
||||||
if self.media_type == 'movie':
|
# if self.media_type == 'movie':
|
||||||
provider_name = 'IMDb'
|
# provider_name = 'IMDb'
|
||||||
elif self.media_type in ('show', 'season', 'episode'):
|
# elif self.media_type in ('show', 'season', 'episode'):
|
||||||
provider_name = 'TheTVDB'
|
# provider_name = 'TheTVDB'
|
||||||
elif self.media_type in ('artist', 'album', 'track'):
|
# elif self.media_type in ('artist', 'album', 'track'):
|
||||||
provider_name = 'Last.fm'
|
# provider_name = 'Last.fm'
|
||||||
return provider_name
|
return provider_name
|
||||||
|
|
||||||
def get_provider_link(self, provider=None):
|
def get_provider_link(self, provider=None):
|
||||||
@@ -697,13 +688,13 @@ class PrettyMetadata(object):
|
|||||||
provider_link = self.get_plex_url()
|
provider_link = self.get_plex_url()
|
||||||
elif provider:
|
elif provider:
|
||||||
provider_link = self.parameters.get(provider + '_url', '')
|
provider_link = self.parameters.get(provider + '_url', '')
|
||||||
else:
|
# else:
|
||||||
if self.media_type == 'movie':
|
# if self.media_type == 'movie':
|
||||||
provider_link = self.parameters.get('imdb_url', '')
|
# provider_link = self.parameters.get('imdb_url', '')
|
||||||
elif self.media_type in ('show', 'season', 'episode'):
|
# elif self.media_type in ('show', 'season', 'episode'):
|
||||||
provider_link = self.parameters.get('thetvdb_url', '')
|
# provider_link = self.parameters.get('thetvdb_url', '')
|
||||||
elif self.media_type in ('artist', 'album', 'track'):
|
# elif self.media_type in ('artist', 'album', 'track'):
|
||||||
provider_link = self.parameters.get('lastfm_url', '')
|
# provider_link = self.parameters.get('lastfm_url', '')
|
||||||
return provider_link
|
return provider_link
|
||||||
|
|
||||||
def get_caption(self, provider):
|
def get_caption(self, provider):
|
||||||
@@ -711,6 +702,7 @@ class PrettyMetadata(object):
|
|||||||
return 'View on ' + provider_name
|
return 'View on ' + provider_name
|
||||||
|
|
||||||
def get_title(self, divider='-'):
|
def get_title(self, divider='-'):
|
||||||
|
title = ''
|
||||||
if self.media_type == 'movie':
|
if self.media_type == 'movie':
|
||||||
title = '%s (%s)' % (self.parameters['title'], self.parameters['year'])
|
title = '%s (%s)' % (self.parameters['title'], self.parameters['year'])
|
||||||
elif self.media_type == 'show':
|
elif self.media_type == 'show':
|
||||||
@@ -728,7 +720,7 @@ class PrettyMetadata(object):
|
|||||||
elif self.media_type == 'album':
|
elif self.media_type == 'album':
|
||||||
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
|
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
|
||||||
elif self.media_type == 'track':
|
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")
|
return title.encode("utf-8")
|
||||||
|
|
||||||
def get_description(self):
|
def get_description(self):
|
||||||
@@ -807,7 +799,7 @@ class Notifier(object):
|
|||||||
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
||||||
verify_msg = " Verify you notification agent settings are correct."
|
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:
|
if err_msg:
|
||||||
logger.error(u"Tautulli Notifiers :: {}".format(err_msg))
|
logger.error(u"Tautulli Notifiers :: {}".format(err_msg))
|
||||||
@@ -1145,7 +1137,8 @@ class DISCORD(Notifier):
|
|||||||
plex_url = pretty_metadata.get_plex_url()
|
plex_url = pretty_metadata.get_plex_url()
|
||||||
|
|
||||||
# Build Discord post attachment
|
# Build Discord post attachment
|
||||||
attachment = {'title': title
|
attachment = {'title': title,
|
||||||
|
'timestamp': helpers.utc_now_iso()
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config['color']:
|
if self.config['color']:
|
||||||
@@ -1250,7 +1243,7 @@ class DISCORD(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'discord_movie_provider',
|
'name': 'discord_movie_provider',
|
||||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -1258,7 +1251,7 @@ class DISCORD(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'discord_tv_provider',
|
'name': 'discord_tv_provider',
|
||||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -1266,7 +1259,7 @@ class DISCORD(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'discord_music_provider',
|
'name': 'discord_music_provider',
|
||||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -1295,11 +1288,19 @@ class EMAIL(Notifier):
|
|||||||
|
|
||||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||||
if self.config['html_support']:
|
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 = MIMEMultipart('alternative')
|
||||||
msg.attach(MIMEText(bleach.clean(body, strip=True), 'plain', 'utf-8'))
|
msg.attach(plain)
|
||||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
msg.attach(html)
|
||||||
else:
|
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['Message-ID'] = email.utils.make_msgid()
|
||||||
msg['Date'] = email.utils.formatdate(localtime=True)
|
msg['Date'] = email.utils.formatdate(localtime=True)
|
||||||
@@ -1310,10 +1311,11 @@ class EMAIL(Notifier):
|
|||||||
|
|
||||||
recipients = self.config['to'] + self.config['cc'] + self.config['bcc']
|
recipients = self.config['to'] + self.config['cc'] + self.config['bcc']
|
||||||
|
|
||||||
|
mailserver = None
|
||||||
success = False
|
success = False
|
||||||
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
|
||||||
mailserver.ehlo()
|
mailserver.ehlo()
|
||||||
|
|
||||||
if self.config['tls']:
|
if self.config['tls']:
|
||||||
@@ -1324,14 +1326,15 @@ class EMAIL(Notifier):
|
|||||||
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
|
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
|
||||||
|
|
||||||
mailserver.sendmail(self.config['from'], recipients, msg.as_string())
|
mailserver.sendmail(self.config['from'], recipients, msg.as_string())
|
||||||
|
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
mailserver.quit()
|
if mailserver:
|
||||||
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
mailserver.quit()
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
@@ -1456,7 +1459,7 @@ class FACEBOOK(Notifier):
|
|||||||
|
|
||||||
return facebook.auth_url(app_id=app_id,
|
return facebook.auth_url(app_id=app_id,
|
||||||
canvas_url=redirect_uri,
|
canvas_url=redirect_uri,
|
||||||
perms=['user_managed_groups','publish_actions'])
|
perms=['publish_to_groups'])
|
||||||
|
|
||||||
def _get_credentials(self, code=''):
|
def _get_credentials(self, code=''):
|
||||||
logger.info(u"Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))
|
logger.info(u"Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))
|
||||||
@@ -1589,7 +1592,7 @@ class FACEBOOK(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'facebook_movie_provider',
|
'name': 'facebook_movie_provider',
|
||||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -1597,7 +1600,7 @@ class FACEBOOK(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'facebook_tv_provider',
|
'name': 'facebook_tv_provider',
|
||||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -1605,7 +1608,7 @@ class FACEBOOK(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'facebook_music_provider',
|
'name': 'facebook_music_provider',
|
||||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -1925,7 +1928,7 @@ class HIPCHAT(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'hipchat_movie_provider',
|
'name': 'hipchat_movie_provider',
|
||||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -1933,7 +1936,7 @@ class HIPCHAT(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'hipchat_tv_provider',
|
'name': 'hipchat_tv_provider',
|
||||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -1941,7 +1944,7 @@ class HIPCHAT(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'hipchat_music_provider',
|
'name': 'hipchat_music_provider',
|
||||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -2133,7 +2136,7 @@ class JOIN(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'join_movie_provider',
|
'name': 'join_movie_provider',
|
||||||
'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>'
|
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -2141,7 +2144,7 @@ class JOIN(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'join_tv_provider',
|
'name': 'join_tv_provider',
|
||||||
'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -2149,7 +2152,7 @@ class JOIN(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'join_music_provider',
|
'name': 'join_music_provider',
|
||||||
'description': 'Select the source for music links in the notificaation. Leave blank for default.',
|
'description': 'Select the source for music links in the notification. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -2439,7 +2442,8 @@ class PLEX(Notifier):
|
|||||||
else:
|
else:
|
||||||
return request.request_content(url)
|
return request.request_content(url)
|
||||||
|
|
||||||
def _sendjson(self, host, method, params={}):
|
def _sendjson(self, host, method, params=None):
|
||||||
|
params = params or {}
|
||||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {'Content-Type': 'application/json'}
|
||||||
url = host + '/jsonrpc'
|
url = host + '/jsonrpc'
|
||||||
@@ -2917,7 +2921,7 @@ class PUSHOVER(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'pushover_movie_provider',
|
'name': 'pushover_movie_provider',
|
||||||
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
|
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -2925,7 +2929,7 @@ class PUSHOVER(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'pushover_tv_provider',
|
'name': 'pushover_tv_provider',
|
||||||
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -2933,7 +2937,7 @@ class PUSHOVER(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'pushover_music_provider',
|
'name': 'pushover_music_provider',
|
||||||
'description': 'Select the source for music links in the notification. Leave blank for default.',
|
'description': 'Select the source for music links in the notification. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -3292,7 +3296,7 @@ class SLACK(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'slack_movie_provider',
|
'name': 'slack_movie_provider',
|
||||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -3300,7 +3304,7 @@ class SLACK(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'slack_tv_provider',
|
'name': 'slack_tv_provider',
|
||||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -3308,7 +3312,7 @@ class SLACK(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'slack_music_provider',
|
'name': 'slack_music_provider',
|
||||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -3468,7 +3472,7 @@ class TWITTER(Notifier):
|
|||||||
poster_url = parameters.get('poster_url','')
|
poster_url = parameters.get('poster_url','')
|
||||||
|
|
||||||
# Hack to add media type to attachment
|
# Hack to add media type to attachment
|
||||||
if poster_url:
|
if poster_url and not helpers.get_img_service():
|
||||||
poster_url += '.png'
|
poster_url += '.png'
|
||||||
|
|
||||||
if self.config['incl_subject']:
|
if self.config['incl_subject']:
|
||||||
@@ -3542,7 +3546,8 @@ class XBMC(Notifier):
|
|||||||
else:
|
else:
|
||||||
return request.request_content(url)
|
return request.request_content(url)
|
||||||
|
|
||||||
def _sendjson(self, host, method, params={}):
|
def _sendjson(self, host, method, params=None):
|
||||||
|
params = params or {}
|
||||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {'Content-Type': 'application/json'}
|
||||||
url = host + '/jsonrpc'
|
url = host + '/jsonrpc'
|
||||||
@@ -3703,7 +3708,7 @@ class ZAPIER(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'zapier_movie_provider',
|
'name': 'zapier_movie_provider',
|
||||||
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
|
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -3711,7 +3716,7 @@ class ZAPIER(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'zapier_tv_provider',
|
'name': 'zapier_tv_provider',
|
||||||
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
|
||||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -3719,7 +3724,7 @@ class ZAPIER(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'zapier_music_provider',
|
'name': 'zapier_music_provider',
|
||||||
'description': 'Select the source for music links in the notification. Leave blank for default.',
|
'description': 'Select the source for music links in the notification. Leave blank to disable.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
|
@@ -49,6 +49,7 @@ def extract_plexivity_xml(xml=None):
|
|||||||
grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey')
|
grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey')
|
||||||
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
|
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
|
||||||
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
|
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
|
||||||
|
original_title = helpers.get_xml_attr(a, 'originalTitle')
|
||||||
guid = helpers.get_xml_attr(a, 'guid')
|
guid = helpers.get_xml_attr(a, 'guid')
|
||||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||||
media_index = helpers.get_xml_attr(a, 'index')
|
media_index = helpers.get_xml_attr(a, 'index')
|
||||||
@@ -180,9 +181,10 @@ def extract_plexivity_xml(xml=None):
|
|||||||
'duration': duration,
|
'duration': duration,
|
||||||
'grandparent_rating_key': grandparent_rating_key,
|
'grandparent_rating_key': grandparent_rating_key,
|
||||||
'grandparent_thumb': grandparent_thumb,
|
'grandparent_thumb': grandparent_thumb,
|
||||||
'grandparent_title': grandparent_title,
|
|
||||||
'parent_title': parent_title,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
|
'parent_title': parent_title,
|
||||||
|
'grandparent_title': grandparent_title,
|
||||||
|
'original_title': original_title,
|
||||||
'tagline': tagline,
|
'tagline': tagline,
|
||||||
'guid': guid,
|
'guid': guid,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
@@ -339,6 +341,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
|
|||||||
'title': row['title'],
|
'title': row['title'],
|
||||||
'parent_title': extracted_xml['parent_title'],
|
'parent_title': extracted_xml['parent_title'],
|
||||||
'grandparent_title': row['grandparent_title'],
|
'grandparent_title': row['grandparent_title'],
|
||||||
|
'original_title': extracted_xml['original_title'],
|
||||||
'full_title': row['full_title'],
|
'full_title': row['full_title'],
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'user': row['user'],
|
'user': row['user'],
|
||||||
@@ -380,6 +383,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
|
|||||||
'title': row['title'],
|
'title': row['title'],
|
||||||
'parent_title': extracted_xml['parent_title'],
|
'parent_title': extracted_xml['parent_title'],
|
||||||
'grandparent_title': row['grandparent_title'],
|
'grandparent_title': row['grandparent_title'],
|
||||||
|
'original_title': extracted_xml['original_title'],
|
||||||
'media_index': extracted_xml['media_index'],
|
'media_index': extracted_xml['media_index'],
|
||||||
'parent_media_index': extracted_xml['parent_media_index'],
|
'parent_media_index': extracted_xml['parent_media_index'],
|
||||||
'thumb': extracted_xml['thumb'],
|
'thumb': extracted_xml['thumb'],
|
||||||
|
@@ -45,6 +45,7 @@ def extract_plexwatch_xml(xml=None):
|
|||||||
duration = helpers.get_xml_attr(a, 'duration')
|
duration = helpers.get_xml_attr(a, 'duration')
|
||||||
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
|
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
|
||||||
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
|
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
|
||||||
|
original_title = helpers.get_xml_attr(a, 'originalTitle')
|
||||||
guid = helpers.get_xml_attr(a, 'guid')
|
guid = helpers.get_xml_attr(a, 'guid')
|
||||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||||
media_index = helpers.get_xml_attr(a, 'index')
|
media_index = helpers.get_xml_attr(a, 'index')
|
||||||
@@ -172,9 +173,10 @@ def extract_plexwatch_xml(xml=None):
|
|||||||
'art': art,
|
'art': art,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'grandparent_thumb': grandparent_thumb,
|
'grandparent_thumb': grandparent_thumb,
|
||||||
'grandparent_title': grandparent_title,
|
|
||||||
'parent_title': parent_title,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
|
'parent_title': parent_title,
|
||||||
|
'grandparent_title': grandparent_title,
|
||||||
|
'original_title': original_title,
|
||||||
'tagline': tagline,
|
'tagline': tagline,
|
||||||
'guid': guid,
|
'guid': guid,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
@@ -332,6 +334,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
|||||||
'title': row['title'],
|
'title': row['title'],
|
||||||
'parent_title': extracted_xml['parent_title'],
|
'parent_title': extracted_xml['parent_title'],
|
||||||
'grandparent_title': row['grandparent_title'],
|
'grandparent_title': row['grandparent_title'],
|
||||||
|
'original_title': extracted_xml['original_title'],
|
||||||
'full_title': row['full_title'],
|
'full_title': row['full_title'],
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'user': row['user'],
|
'user': row['user'],
|
||||||
@@ -373,6 +376,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
|||||||
'title': row['title'],
|
'title': row['title'],
|
||||||
'parent_title': extracted_xml['parent_title'],
|
'parent_title': extracted_xml['parent_title'],
|
||||||
'grandparent_title': row['grandparent_title'],
|
'grandparent_title': row['grandparent_title'],
|
||||||
|
'original_title': extracted_xml['original_title'],
|
||||||
'media_index': extracted_xml['media_index'],
|
'media_index': extracted_xml['media_index'],
|
||||||
'parent_media_index': extracted_xml['parent_media_index'],
|
'parent_media_index': extracted_xml['parent_media_index'],
|
||||||
'thumb': extracted_xml['thumb'],
|
'thumb': extracted_xml['thumb'],
|
||||||
|
@@ -19,6 +19,7 @@ import time
|
|||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
|
import activity_processor
|
||||||
import common
|
import common
|
||||||
import helpers
|
import helpers
|
||||||
import http_handler
|
import http_handler
|
||||||
@@ -482,6 +483,7 @@ class PmsConnect(object):
|
|||||||
actors = []
|
actors = []
|
||||||
genres = []
|
genres = []
|
||||||
labels = []
|
labels = []
|
||||||
|
collections = []
|
||||||
|
|
||||||
if m.getElementsByTagName('Director'):
|
if m.getElementsByTagName('Director'):
|
||||||
for director in m.getElementsByTagName('Director'):
|
for director in m.getElementsByTagName('Director'):
|
||||||
@@ -503,6 +505,10 @@ class PmsConnect(object):
|
|||||||
for label in m.getElementsByTagName('Label'):
|
for label in m.getElementsByTagName('Label'):
|
||||||
labels.append(helpers.get_xml_attr(label, 'tag'))
|
labels.append(helpers.get_xml_attr(label, 'tag'))
|
||||||
|
|
||||||
|
if m.getElementsByTagName('Collection'):
|
||||||
|
for collection in m.getElementsByTagName('Collection'):
|
||||||
|
collections.append(helpers.get_xml_attr(collection, 'tag'))
|
||||||
|
|
||||||
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
|
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
|
||||||
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
|
||||||
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
|
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
|
||||||
@@ -512,6 +518,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(m, 'title'),
|
'title': helpers.get_xml_attr(m, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
||||||
|
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
|
||||||
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
|
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(m, 'index'),
|
'media_index': helpers.get_xml_attr(m, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
|
||||||
@@ -539,6 +546,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(m, 'title'),
|
'full_title': helpers.get_xml_attr(m, 'title'),
|
||||||
'child_count': helpers.get_xml_attr(m, 'childCount')
|
'child_count': helpers.get_xml_attr(m, 'childCount')
|
||||||
}
|
}
|
||||||
@@ -661,6 +669,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -708,6 +717,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -752,6 +762,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -797,6 +808,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -840,6 +852,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -884,6 +897,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -920,6 +934,8 @@ class PmsConnect(object):
|
|||||||
elif metadata_type == 'track':
|
elif metadata_type == 'track':
|
||||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||||
album_details = self.get_metadata_details(parent_rating_key)
|
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,
|
metadata = {'media_type': metadata_type,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
'library_name': library_name,
|
'library_name': library_name,
|
||||||
@@ -929,6 +945,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -957,8 +974,8 @@ class PmsConnect(object):
|
|||||||
'genres': album_details['genres'],
|
'genres': album_details['genres'],
|
||||||
'labels': album_details['labels'],
|
'labels': album_details['labels'],
|
||||||
'collections': album_details['collections'],
|
'collections': album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title')),
|
track_artist),
|
||||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,6 +989,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -1016,6 +1034,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -1060,6 +1079,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -1105,6 +1125,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'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'),
|
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
@@ -1658,6 +1679,8 @@ class PmsConnect(object):
|
|||||||
'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'),
|
'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'),
|
||||||
'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
|
'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
|
||||||
'synced_version': 1 if sync_id else 0,
|
'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'),
|
'indexes': int(indexes == 'sd'),
|
||||||
'bif_thumb': bif_thumb,
|
'bif_thumb': bif_thumb,
|
||||||
'subtitles': 1 if subtitle_id and subtitle_selected else 0
|
'subtitles': 1 if subtitle_id and subtitle_selected else 0
|
||||||
@@ -1670,9 +1693,7 @@ class PmsConnect(object):
|
|||||||
if not helpers.get_xml_attr(session, 'ratingKey').isdigit():
|
if not helpers.get_xml_attr(session, 'ratingKey').isdigit():
|
||||||
channel_stream = 1
|
channel_stream = 1
|
||||||
|
|
||||||
clip_media = session.getElementsByTagName('Media')[0]
|
audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels')
|
||||||
clip_part = clip_media.getElementsByTagName('Part')[0]
|
|
||||||
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
|
|
||||||
metadata_details = {'media_type': media_type,
|
metadata_details = {'media_type': media_type,
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
'library_name': helpers.get_xml_attr(session, 'librarySectionTitle'),
|
'library_name': helpers.get_xml_attr(session, 'librarySectionTitle'),
|
||||||
@@ -1682,6 +1703,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(session, 'title'),
|
'title': helpers.get_xml_attr(session, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
|
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
|
||||||
|
'original_title': helpers.get_xml_attr(session, 'originalTitle'),
|
||||||
'sort_title': helpers.get_xml_attr(session, 'titleSort'),
|
'sort_title': helpers.get_xml_attr(session, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||||
@@ -1710,18 +1732,17 @@ class PmsConnect(object):
|
|||||||
'genres': [],
|
'genres': [],
|
||||||
'labels': [],
|
'labels': [],
|
||||||
'full_title': helpers.get_xml_attr(session, 'title'),
|
'full_title': helpers.get_xml_attr(session, 'title'),
|
||||||
'container': helpers.get_xml_attr(clip_media, 'container') \
|
'container': helpers.get_xml_attr(stream_media_info, 'container') \
|
||||||
or helpers.get_xml_attr(clip_part, 'container'),
|
or helpers.get_xml_attr(stream_media_parts_info, 'container'),
|
||||||
'height': helpers.get_xml_attr(clip_media, 'height'),
|
'height': helpers.get_xml_attr(stream_media_info, 'height'),
|
||||||
'width': helpers.get_xml_attr(clip_media, 'width'),
|
'width': helpers.get_xml_attr(stream_media_info, 'width'),
|
||||||
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
|
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
|
||||||
'video_resolution': helpers.get_xml_attr(clip_media, 'videoResolution'),
|
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution'),
|
||||||
'audio_codec': helpers.get_xml_attr(clip_media, 'audioCodec'),
|
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
|
||||||
'audio_channels': audio_channels,
|
'audio_channels': audio_channels,
|
||||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||||
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
||||||
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
|
'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'),
|
'extra_type': helpers.get_xml_attr(session, 'extraType'),
|
||||||
'sub_type': helpers.get_xml_attr(session, 'subtype')
|
'sub_type': helpers.get_xml_attr(session, 'subtype')
|
||||||
}
|
}
|
||||||
@@ -1790,13 +1811,12 @@ class PmsConnect(object):
|
|||||||
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
||||||
|
|
||||||
# Overrides for live sessions
|
# 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_decision'] = 'transcode'
|
||||||
stream_details['stream_container'] = transcode_details['transcode_container']
|
stream_details['stream_container'] = transcode_details['transcode_container']
|
||||||
|
|
||||||
video_details['stream_video_decision'] = transcode_details['video_decision']
|
video_details['stream_video_decision'] = transcode_details['video_decision']
|
||||||
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
|
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']
|
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
|
||||||
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
|
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
|
||||||
@@ -1906,7 +1926,7 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
return session_output
|
return session_output
|
||||||
|
|
||||||
def terminate_session(self, session_id='', message=''):
|
def terminate_session(self, session_key='', session_id='', message=''):
|
||||||
"""
|
"""
|
||||||
Terminates a streaming session.
|
Terminates a streaming session.
|
||||||
|
|
||||||
@@ -1914,10 +1934,22 @@ class PmsConnect(object):
|
|||||||
"""
|
"""
|
||||||
message = message or 'The server owner has ended the stream.'
|
message = message or 'The server owner has ended the stream.'
|
||||||
|
|
||||||
|
if session_key and not session_id:
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
session = ap.get_session_by_key(session_key=session_key)
|
||||||
|
session_id = session['session_id']
|
||||||
|
|
||||||
|
elif session_id and not session_key:
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
session = ap.get_session_by_id(session_id=session_id)
|
||||||
|
session_key = session['session_key']
|
||||||
|
|
||||||
if session_id:
|
if session_id:
|
||||||
|
logger.info(u"Tautulli Pmsconnect :: Terminating session %s (session_id %s)." % (session_key, session_id))
|
||||||
result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message))
|
result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message))
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
|
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session %s. Missing session_id." % session_key)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_item_children(self, rating_key='', get_grandchildren=False):
|
def get_item_children(self, rating_key='', get_grandchildren=False):
|
||||||
@@ -1994,6 +2026,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(m, 'title'),
|
'title': helpers.get_xml_attr(m, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
||||||
|
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
|
||||||
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
|
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(m, 'index'),
|
'media_index': helpers.get_xml_attr(m, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
|
||||||
@@ -2311,6 +2344,7 @@ class PmsConnect(object):
|
|||||||
'title': helpers.get_xml_attr(item, 'title'),
|
'title': helpers.get_xml_attr(item, 'title'),
|
||||||
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
||||||
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
|
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
|
||||||
|
'original_title': helpers.get_xml_attr(item, 'originalTitle'),
|
||||||
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
|
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
|
||||||
'media_index': helpers.get_xml_attr(item, 'index'),
|
'media_index': helpers.get_xml_attr(item, 'index'),
|
||||||
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
|
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
|
||||||
@@ -2649,7 +2683,7 @@ class PmsConnect(object):
|
|||||||
child_title = helpers.get_xml_attr(item, 'title')
|
child_title = helpers.get_xml_attr(item, 'title')
|
||||||
|
|
||||||
if child_rating_key:
|
if child_rating_key:
|
||||||
key = int(child_index)
|
key = int(child_index) if child_index else child_title
|
||||||
children.update({key: {'rating_key': int(child_rating_key)}})
|
children.update({key: {'rating_key': int(child_rating_key)}})
|
||||||
|
|
||||||
key = int(parent_index) if match_type == 'index' else parent_title
|
key = int(parent_index) if match_type == 'index' else parent_title
|
||||||
@@ -2661,9 +2695,9 @@ class PmsConnect(object):
|
|||||||
key = 0 if match_type == 'index' else title
|
key = 0 if match_type == 'index' else title
|
||||||
key_list = {key: {'rating_key': int(rating_key),
|
key_list = {key: {'rating_key': int(rating_key),
|
||||||
'children': parents},
|
'children': parents},
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
'library_name': library_name
|
'library_name': library_name
|
||||||
}
|
}
|
||||||
|
|
||||||
return key_list
|
return key_list
|
||||||
|
|
||||||
|
@@ -201,9 +201,10 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
|||||||
'grandparent_thumb': common.DEFAULT_POSTER_THUMB,
|
'grandparent_thumb': common.DEFAULT_POSTER_THUMB,
|
||||||
'thumb': common.DEFAULT_POSTER_THUMB,
|
'thumb': common.DEFAULT_POSTER_THUMB,
|
||||||
'bif_thumb': '',
|
'bif_thumb': '',
|
||||||
'grandparent_title': 'Plex Media',
|
|
||||||
'parent_title': 'Plex Media',
|
|
||||||
'title': 'Plex Media',
|
'title': 'Plex Media',
|
||||||
|
'parent_title': 'Plex Media',
|
||||||
|
'grandparent_title': 'Plex Media',
|
||||||
|
'original_title': 'Plex Media',
|
||||||
'rating_key': '',
|
'rating_key': '',
|
||||||
'parent_rating_key': '',
|
'parent_rating_key': '',
|
||||||
'grandparent_rating_key': '',
|
'grandparent_rating_key': '',
|
||||||
|
@@ -521,7 +521,8 @@ class Users(object):
|
|||||||
if str(user_id).isdigit():
|
if str(user_id).isdigit():
|
||||||
query = 'SELECT session_history.id, session_history.media_type, ' \
|
query = 'SELECT session_history.id, session_history.media_type, ' \
|
||||||
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
|
'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 ' \
|
'year, started, user ' \
|
||||||
'FROM session_history_metadata ' \
|
'FROM session_history_metadata ' \
|
||||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||||
@@ -552,6 +553,7 @@ class Users(object):
|
|||||||
'title': row['title'],
|
'title': row['title'],
|
||||||
'parent_title': row['parent_title'],
|
'parent_title': row['parent_title'],
|
||||||
'grandparent_title': row['grandparent_title'],
|
'grandparent_title': row['grandparent_title'],
|
||||||
|
'original_title': row['original_title'],
|
||||||
'thumb': thumb,
|
'thumb': thumb,
|
||||||
'media_index': row['media_index'],
|
'media_index': row['media_index'],
|
||||||
'parent_media_index': row['parent_media_index'],
|
'parent_media_index': row['parent_media_index'],
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.1.6-beta"
|
PLEXPY_RELEASE_VERSION = "v2.1.12"
|
||||||
|
@@ -18,6 +18,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
|
import urllib
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib.static import serve_file, serve_download
|
from cherrypy.lib.static import serve_file, serve_download
|
||||||
@@ -56,7 +57,7 @@ import web_socket
|
|||||||
from plexpy.api2 import API2
|
from plexpy.api2 import API2
|
||||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json
|
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.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):
|
def serve_template(templatename, **kwargs):
|
||||||
@@ -247,23 +248,23 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def terminate_session(self, session_id=None, message=None, **kwargs):
|
def terminate_session(self, session_key=None, session_id=None, message=None, **kwargs):
|
||||||
""" Add a new notification agent.
|
""" Stop a streaming session.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
session_id (str): The id of the session to terminate
|
session_key (int): The session key of the session to terminate, OR
|
||||||
message (str): A custom message to send to the client
|
session_id (str): The session id of the session to terminate
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
None
|
message (str): A custom message to send to the client
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
result = pms_connect.terminate_session(session_id=session_id, message=message)
|
result = pms_connect.terminate_session(session_key=session_key, session_id=session_id, message=message)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return {'result': 'success', 'message': 'Session terminated.'}
|
return {'result': 'success', 'message': 'Session terminated.'}
|
||||||
@@ -273,8 +274,21 @@ class WebInterface(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def return_sessions_url(self, **kwargs):
|
def return_plex_xml_url(self, endpoint='', plextv=False, **kwargs):
|
||||||
return plexpy.CONFIG.PMS_URL + '/status/sessions?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN
|
kwargs['X-Plex-Token'] = plexpy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
|
if plextv:
|
||||||
|
base_url = 'https://plex.tv'
|
||||||
|
else:
|
||||||
|
if plexpy.CONFIG.PMS_URL_OVERRIDE:
|
||||||
|
base_url = plexpy.CONFIG.PMS_URL_OVERRIDE
|
||||||
|
else:
|
||||||
|
base_url = plexpy.CONFIG.PMS_URL
|
||||||
|
|
||||||
|
if '{machine_id}' in endpoint:
|
||||||
|
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
|
||||||
|
|
||||||
|
return base_url + endpoint + '?' + urllib.urlencode(kwargs)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
@@ -1614,6 +1628,7 @@ class WebInterface(object):
|
|||||||
"full_title": "Game of Thrones - The Red Woman",
|
"full_title": "Game of Thrones - The Red Woman",
|
||||||
"grandparent_rating_key": 351,
|
"grandparent_rating_key": 351,
|
||||||
"grandparent_title": "Game of Thrones",
|
"grandparent_title": "Game of Thrones",
|
||||||
|
"original_title": "",
|
||||||
"group_count": 1,
|
"group_count": 1,
|
||||||
"group_ids": "1124",
|
"group_ids": "1124",
|
||||||
"id": 1124,
|
"id": 1124,
|
||||||
@@ -1715,6 +1730,77 @@ class WebInterface(object):
|
|||||||
|
|
||||||
return serve_template(templatename="stream_data.html", title="Stream Data", data=stream_data, user=user)
|
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
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
def get_ip_address_details(self, ip_address=None, **kwargs):
|
def get_ip_address_details(self, ip_address=None, **kwargs):
|
||||||
@@ -2756,6 +2842,9 @@ class WebInterface(object):
|
|||||||
"show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS,
|
"show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS,
|
||||||
"newsletter_dir": plexpy.CONFIG.NEWSLETTER_DIR,
|
"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
|
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2778,7 +2867,7 @@ class WebInterface(object):
|
|||||||
"allow_guest_access", "cache_images", "http_proxy", "http_basic_auth", "notify_concurrent_by_ip",
|
"allow_guest_access", "cache_images", "http_proxy", "http_basic_auth", "notify_concurrent_by_ip",
|
||||||
"history_table_activity", "plexpy_auto_update",
|
"history_table_activity", "plexpy_auto_update",
|
||||||
"themoviedb_lookup", "tvmaze_lookup", "http_plex_admin",
|
"themoviedb_lookup", "tvmaze_lookup", "http_plex_admin",
|
||||||
"newsletter_self_hosted"
|
"newsletter_self_hosted", "newsletter_inline_styles"
|
||||||
]
|
]
|
||||||
for checked_config in checked_configs:
|
for checked_config in checked_configs:
|
||||||
if checked_config not in kwargs:
|
if checked_config not in kwargs:
|
||||||
@@ -3149,7 +3238,7 @@ class WebInterface(object):
|
|||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def set_notifier_config(self, notifier_id=None, agent_id=None, **kwargs):
|
def set_notifier_config(self, notifier_id=None, agent_id=None, **kwargs):
|
||||||
""" Configure an exisitng notificaiton agent.
|
""" Configure an existing notification agent.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
@@ -3268,10 +3357,10 @@ class WebInterface(object):
|
|||||||
return {'result': 'success', 'message': 'Notification queued.'}
|
return {'result': 'success', 'message': 'Notification queued.'}
|
||||||
else:
|
else:
|
||||||
logger.debug(u"Unable to send %snotification, invalid notifier_id %s." % (test, notifier_id))
|
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:
|
else:
|
||||||
logger.debug(u"Unable to send %snotification, no notifier_id received." % test)
|
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.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@@ -3408,7 +3497,7 @@ class WebInterface(object):
|
|||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def set_mobile_device_config(self, mobile_device_id=None, **kwargs):
|
def set_mobile_device_config(self, mobile_device_id=None, **kwargs):
|
||||||
""" Configure an exisitng notificaiton agent.
|
""" Configure an existing notification agent.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
@@ -4032,6 +4121,15 @@ class WebInterface(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def image(self, *args, **kwargs):
|
def image(self, *args, **kwargs):
|
||||||
if args:
|
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]
|
img_hash = args[0].split('.')[0]
|
||||||
|
|
||||||
if img_hash in ('poster', 'cover', 'art'):
|
if img_hash in ('poster', 'cover', 'art'):
|
||||||
@@ -4049,7 +4147,7 @@ class WebInterface(object):
|
|||||||
|
|
||||||
if img_info:
|
if img_info:
|
||||||
kwargs.update(img_info)
|
kwargs.update(img_info)
|
||||||
return self.real_pms_image_proxy(**kwargs)
|
return self.real_pms_image_proxy(refresh=True, **kwargs)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -4170,16 +4268,18 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@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.
|
""" Delete the images uploaded to image hosting services.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
rating_key (int): 1234
|
rating_key (int): 1234
|
||||||
(Note: Must be the movie, show, season, artist, or album rating key)
|
(Note: Must be the movie, show, season, artist, or album rating key)
|
||||||
Optional parameters:
|
service (str): 'imgur' or 'cloudinary'
|
||||||
service (str): imgur or cloudinary
|
delete_all (bool): 'true' to delete all images form the service
|
||||||
(Note: Defaults to service in Image Hosting setting)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
@@ -4188,8 +4288,10 @@ class WebInterface(object):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
delete_all = (delete_all == 'true')
|
||||||
|
|
||||||
data_factory = datafactory.DataFactory()
|
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:
|
if result:
|
||||||
return {'result': 'success', 'message': 'Deleted hosted images from %s.' % result.capitalize()}
|
return {'result': 'success', 'message': 'Deleted hosted images from %s.' % result.capitalize()}
|
||||||
@@ -4540,6 +4642,7 @@ class WebInterface(object):
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"media_type": "episode",
|
"media_type": "episode",
|
||||||
|
"original_title": "",
|
||||||
"originally_available_at": "2016-04-24",
|
"originally_available_at": "2016-04-24",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
"parent_rating_key": "153036",
|
"parent_rating_key": "153036",
|
||||||
@@ -4598,6 +4701,7 @@ class WebInterface(object):
|
|||||||
"library_name": "",
|
"library_name": "",
|
||||||
"media_index": "1",
|
"media_index": "1",
|
||||||
"media_type": "episode",
|
"media_type": "episode",
|
||||||
|
"original_title": "",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
"parent_rating_key": "153036",
|
"parent_rating_key": "153036",
|
||||||
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
||||||
@@ -4869,6 +4973,7 @@ class WebInterface(object):
|
|||||||
"optimized_version_profile": "",
|
"optimized_version_profile": "",
|
||||||
"optimized_version_title": "",
|
"optimized_version_title": "",
|
||||||
"originally_available_at": "2016-04-24",
|
"originally_available_at": "2016-04-24",
|
||||||
|
"original_title": "",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
"parent_rating_key": "153036",
|
"parent_rating_key": "153036",
|
||||||
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
||||||
@@ -5468,7 +5573,7 @@ class WebInterface(object):
|
|||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
[{"id": 1,
|
[{"id": 1,
|
||||||
"agent_id": 13,
|
"agent_id": 0,
|
||||||
"agent_name": "recently_added",
|
"agent_name": "recently_added",
|
||||||
"agent_label": "Recently Added",
|
"agent_label": "Recently Added",
|
||||||
"friendly_name": "",
|
"friendly_name": "",
|
||||||
@@ -5528,15 +5633,24 @@ class WebInterface(object):
|
|||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
{"id": 1,
|
{"id": 1,
|
||||||
"agent_id": 13,
|
"agent_id": 0,
|
||||||
"agent_name": "recently_added",
|
"agent_name": "recently_added",
|
||||||
"agent_label": "Recently Added",
|
"agent_label": "Recently Added",
|
||||||
"friendly_name": "",
|
"friendly_name": "",
|
||||||
|
"id_name": "",
|
||||||
"cron": "0 0 * * 1",
|
"cron": "0 0 * * 1",
|
||||||
"active": 1
|
"active": 1,
|
||||||
"config": {"time_frame": 7,
|
"subject": "Recently Added to {server_name}! ({end_date})",
|
||||||
"time_frame_units": "days",
|
"body": "View the newsletter here: {newsletter_url}",
|
||||||
"incl_libraries": [1, 2]
|
"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": {...},
|
"email_config": {...},
|
||||||
"config_options": [{...}, ...],
|
"config_options": [{...}, ...],
|
||||||
@@ -5583,19 +5697,15 @@ class WebInterface(object):
|
|||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
|
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
|
||||||
""" Configure an exisitng notificaiton agent.
|
""" Configure an existing newsletter agent.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required parameters:
|
||||||
newsletter_id (int): The newsletter config to update
|
newsletter_id (int): The newsletter config to update
|
||||||
agent_id (int): The newsletter type of the newsletter
|
agent_id (int): The newsletter type of the newsletter
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
Pass all the config options for the agent with the agent prefix:
|
Pass all the config options for the agent with the 'newsletter_config_' and 'newsletter_email_' prefix.
|
||||||
e.g. For Recently Added: recently_added_last_days
|
|
||||||
recently_added_incl_movies
|
|
||||||
recently_added_incl_shows
|
|
||||||
recently_added_incl_artists
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
@@ -5613,7 +5723,6 @@ class WebInterface(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi("notify_newsletter")
|
|
||||||
def send_newsletter(self, newsletter_id=None, subject='', body='', message='', notify_action='', **kwargs):
|
def send_newsletter(self, newsletter_id=None, subject='', body='', message='', notify_action='', **kwargs):
|
||||||
""" Send a newsletter using Tautulli.
|
""" Send a newsletter using Tautulli.
|
||||||
|
|
||||||
@@ -5653,7 +5762,29 @@ class WebInterface(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def newsletter(self, *args, **kwargs):
|
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:
|
if args:
|
||||||
|
# Keep this for backwards compatibility for images through /newsletter/image
|
||||||
if len(args) >= 2 and args[0] == 'image':
|
if len(args) >= 2 and args[0] == 'image':
|
||||||
if args[1] == 'images':
|
if args[1] == 'images':
|
||||||
resource_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/default/')
|
resource_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/default/')
|
||||||
@@ -5662,8 +5793,7 @@ class WebInterface(object):
|
|||||||
except NotFound:
|
except NotFound:
|
||||||
return
|
return
|
||||||
|
|
||||||
cherrypy.response.headers['Cache-Control'] = 'max-age=2592000' # 30 days
|
return self.image(args[1])
|
||||||
return self.image(args[1], refresh=True)
|
|
||||||
|
|
||||||
if len(args) >= 2 and args[0] == 'id':
|
if len(args) >= 2 and args[0] == 'id':
|
||||||
newsletter_id_name = args[1]
|
newsletter_id_name = args[1]
|
||||||
@@ -5687,7 +5817,7 @@ class WebInterface(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def real_newsletter(self, newsletter_id=None, start_date=None, end_date=None,
|
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':
|
if newsletter_id and newsletter_id != 'None':
|
||||||
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||||
|
|
||||||
@@ -5702,14 +5832,13 @@ class WebInterface(object):
|
|||||||
body=newsletter['body'],
|
body=newsletter['body'],
|
||||||
message=newsletter['message'])
|
message=newsletter['message'])
|
||||||
preview = (preview == 'true')
|
preview = (preview == 'true')
|
||||||
master = (master == 'true')
|
|
||||||
raw = (raw == 'true')
|
raw = (raw == 'true')
|
||||||
|
|
||||||
if raw:
|
if raw:
|
||||||
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||||
return json.dumps(newsletter_agent.raw_data(preview=preview))
|
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)
|
logger.error(u"Failed to retrieve newsletter: Invalid newsletter_id %s" % newsletter_id)
|
||||||
return "Failed to retrieve newsletter: invalid newsletter_id parameter"
|
return "Failed to retrieve newsletter: invalid newsletter_id parameter"
|
||||||
|
Reference in New Issue
Block a user