Compare commits
228 Commits
v2.1.41
...
v2.2.2-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6d5c320701 | ||
![]() |
400a189455 | ||
![]() |
b7d03a4f31 | ||
![]() |
523e6421be | ||
![]() |
e0cd6f7071 | ||
![]() |
38db0b7a70 | ||
![]() |
f39ecd89a7 | ||
![]() |
f7f76d82b6 | ||
![]() |
9097e79e4f | ||
![]() |
88711e7601 | ||
![]() |
d0fa83bb8c | ||
![]() |
1271458f83 | ||
![]() |
2ae09a07e6 | ||
![]() |
33d860384c | ||
![]() |
a4eda99a4a | ||
![]() |
752c7badd2 | ||
![]() |
6399c90642 | ||
![]() |
97089846e9 | ||
![]() |
4de7884e39 | ||
![]() |
440adfb914 | ||
![]() |
5f26d0085d | ||
![]() |
f484604c69 | ||
![]() |
899d2fbf9d | ||
![]() |
6a87dc9c40 | ||
![]() |
104e2929df | ||
![]() |
faac6b11c2 | ||
![]() |
377a23478e | ||
![]() |
c979e78802 | ||
![]() |
38f64c7d85 | ||
![]() |
1091a64863 | ||
![]() |
23de9616f1 | ||
![]() |
198e7767dc | ||
![]() |
aa31bf1a19 | ||
![]() |
f366304c50 | ||
![]() |
ce289995ff | ||
![]() |
ca2b4085c9 | ||
![]() |
1d08069162 | ||
![]() |
bcbfaae630 | ||
![]() |
ae9df92d28 | ||
![]() |
47610323b0 | ||
![]() |
d1f1763919 | ||
![]() |
1326ad8708 | ||
![]() |
6e09e509bd | ||
![]() |
e8d0557852 | ||
![]() |
aac705f465 | ||
![]() |
009971901b | ||
![]() |
1ffd6c0ea1 | ||
![]() |
50ce29cc64 | ||
![]() |
e4ec24be26 | ||
![]() |
04765288d7 | ||
![]() |
8fdd0ba0d9 | ||
![]() |
16ffbd9940 | ||
![]() |
fa61302954 | ||
![]() |
763e5f583a | ||
![]() |
395fc49087 | ||
![]() |
d54794e85f | ||
![]() |
d5917f89f0 | ||
![]() |
1003aa2df5 | ||
![]() |
6205af1a9a | ||
![]() |
d8b1db536c | ||
![]() |
699357ca21 | ||
![]() |
50398049f5 | ||
![]() |
1f83afc2f4 | ||
![]() |
90374bb46f | ||
![]() |
ccdd410eda | ||
![]() |
77bb806a01 | ||
![]() |
a952352e1f | ||
![]() |
b733ce969a | ||
![]() |
f4351df302 | ||
![]() |
76893100fc | ||
![]() |
96e8b808da | ||
![]() |
595bff94b4 | ||
![]() |
5661c00497 | ||
![]() |
a98d7bd4eb | ||
![]() |
097203162d | ||
![]() |
823c9b3159 | ||
![]() |
35965a6a1b | ||
![]() |
8d67cc4c5a | ||
![]() |
42e33a0468 | ||
![]() |
b2529db026 | ||
![]() |
e99c4aec4a | ||
![]() |
2d0a97f259 | ||
![]() |
fabb52763b | ||
![]() |
03dd1a6974 | ||
![]() |
ecee50a5e4 | ||
![]() |
cbab7c4cbf | ||
![]() |
257ea14c59 | ||
![]() |
11299291b0 | ||
![]() |
533b8076e4 | ||
![]() |
b0383b4813 | ||
![]() |
96500f75b0 | ||
![]() |
2a3bd3413f | ||
![]() |
8ec136a0ca | ||
![]() |
2bead0fc29 | ||
![]() |
359776d48a | ||
![]() |
05a16bb199 | ||
![]() |
f457704105 | ||
![]() |
5aa59b93b0 | ||
![]() |
47d51f92c7 | ||
![]() |
068cf17d0a | ||
![]() |
3c1b849a5d | ||
![]() |
8384ec7f15 | ||
![]() |
0e7a3962bb | ||
![]() |
3d1bb9975c | ||
![]() |
eed473be15 | ||
![]() |
e23d9bca22 | ||
![]() |
85a952a5c9 | ||
![]() |
5cc36310ba | ||
![]() |
8690d2ced5 | ||
![]() |
f572943a7b | ||
![]() |
89248ad46a | ||
![]() |
517db71916 | ||
![]() |
ad82de010d | ||
![]() |
02fd9edbbf | ||
![]() |
6c84dd7be8 | ||
![]() |
9edbe6af37 | ||
![]() |
37d09e9bad | ||
![]() |
0d0595e9a0 | ||
![]() |
17477455cb | ||
![]() |
43f594709d | ||
![]() |
8885b3e7e0 | ||
![]() |
c060638539 | ||
![]() |
2144d4d7ed | ||
![]() |
4f2e09d733 | ||
![]() |
9d13c29bf6 | ||
![]() |
60d577f95e | ||
![]() |
1dd1c6f67f | ||
![]() |
bcd0691b33 | ||
![]() |
24a2559ab5 | ||
![]() |
22a6bae4cf | ||
![]() |
2c45de1fe5 | ||
![]() |
0ab93d7a7f | ||
![]() |
c8831efb28 | ||
![]() |
0cbde5a2f5 | ||
![]() |
7de82d87f7 | ||
![]() |
751b97a39c | ||
![]() |
7b58bcc279 | ||
![]() |
820a2e688c | ||
![]() |
10a7f540ad | ||
![]() |
31a6c627af | ||
![]() |
aba4cbf9e4 | ||
![]() |
a5624e86e4 | ||
![]() |
5480d09a0b | ||
![]() |
5fad0a1d97 | ||
![]() |
4a7b5bab54 | ||
![]() |
fe0557dcc1 | ||
![]() |
e4b6d61098 | ||
![]() |
73e01ebaaf | ||
![]() |
d752d46676 | ||
![]() |
3932409fb4 | ||
![]() |
f69d4f1c42 | ||
![]() |
96699fc3b0 | ||
![]() |
97ec8f6828 | ||
![]() |
df851e67f9 | ||
![]() |
5d1bc3cf9b | ||
![]() |
81ab9b006d | ||
![]() |
1699fc09cf | ||
![]() |
f8b6a9f1e8 | ||
![]() |
ba465a0d15 | ||
![]() |
af521b4058 | ||
![]() |
27b512611f | ||
![]() |
b79f165a27 | ||
![]() |
716f76baed | ||
![]() |
18c57a4fc6 | ||
![]() |
c26483b4b8 | ||
![]() |
441e39d776 | ||
![]() |
7ecc075c7e | ||
![]() |
2e42663832 | ||
![]() |
075f9f8cbd | ||
![]() |
f3d42e7b53 | ||
![]() |
fa0b547b32 | ||
![]() |
8f19cdccfd | ||
![]() |
5c207aeee6 | ||
![]() |
d48273ef98 | ||
![]() |
a7803dcad7 | ||
![]() |
4fc9b6fdb4 | ||
![]() |
73f8f83658 | ||
![]() |
66a0f953b3 | ||
![]() |
f189eea32b | ||
![]() |
be29d879a7 | ||
![]() |
e3e906c9e5 | ||
![]() |
6fce31e1b9 | ||
![]() |
db1cb3d658 | ||
![]() |
c0d7c5ddff | ||
![]() |
d0cd2672dd | ||
![]() |
ca55900d40 | ||
![]() |
3d65ffc6d3 | ||
![]() |
f94b796c2b | ||
![]() |
66c1fd6593 | ||
![]() |
13a45facf9 | ||
![]() |
26f10b2c3d | ||
![]() |
9aea4c85b0 | ||
![]() |
ee1b0eeeff | ||
![]() |
c6cfb4a020 | ||
![]() |
91da41ff86 | ||
![]() |
a811edb236 | ||
![]() |
66de806b02 | ||
![]() |
c825176563 | ||
![]() |
7ae87fe0e7 | ||
![]() |
99f826c236 | ||
![]() |
f88d673e87 | ||
![]() |
bc2f73d686 | ||
![]() |
645c2ecef6 | ||
![]() |
4d4a8ca3b2 | ||
![]() |
a98bc45c10 | ||
![]() |
aa5affe366 | ||
![]() |
b9d5e49a71 | ||
![]() |
e90390be67 | ||
![]() |
8ef671c9cb | ||
![]() |
8829516cae | ||
![]() |
d8e8dfbd45 | ||
![]() |
0b6d9a4890 | ||
![]() |
55ffd68023 | ||
![]() |
6380de3e6c | ||
![]() |
d6220a921a | ||
![]() |
10e421b9d4 | ||
![]() |
90056bcce2 | ||
![]() |
4740d0fbf3 | ||
![]() |
13374c9ded | ||
![]() |
2a03be1905 | ||
![]() |
c8f132a750 | ||
![]() |
4d0c4bf4f4 | ||
![]() |
e321c5b197 | ||
![]() |
badbfdc4c1 | ||
![]() |
7d71086a41 | ||
![]() |
0e1764755a | ||
![]() |
c31d3ffd6c | ||
![]() |
0cba3988aa |
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
contrib
|
||||
init-scripts
|
||||
pylintrc
|
||||
*.md
|
||||
!CHANGELOG*.md
|
83
.github/workflows/publish-docker.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Publish Docker
|
||||
on:
|
||||
push:
|
||||
branches: [master, beta, nightly]
|
||||
tags: [v*]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
|
||||
elif [[ $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo ::set-output name=tag::latest
|
||||
else
|
||||
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-output name=branch::master
|
||||
else
|
||||
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
echo ::set-output name=commit::${GITHUB_SHA}
|
||||
echo ::set-output name=build_date::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64,linux/arm
|
||||
echo ::set-output name=docker_image::tautulli/tautulli
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Docker Buildx (no push)
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||
--output "type=image,push=false" \
|
||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
|
||||
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
|
||||
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
|
||||
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
|
||||
--file Dockerfile .
|
||||
|
||||
- name: Docker Login
|
||||
if: success()
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Docker Buildx (push)
|
||||
if: success()
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||
--output "type=image,push=true" \
|
||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
|
||||
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
|
||||
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
|
||||
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
|
||||
--file Dockerfile .
|
||||
|
||||
- name: Clear
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ${HOME}/.docker/config.json
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
job: ${{ github.workflow }}
|
||||
nofail: true
|
28
.github/workflows/publish-release.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Publish Release
|
||||
on:
|
||||
push:
|
||||
tags: [v*]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@master
|
||||
- name: Get Release Version
|
||||
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
- name: Get Changelog
|
||||
run: echo ::set-env name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.RELEASE_VERSION }}
|
||||
release_name: Tautulli ${{ env.RELEASE_VERSION }}
|
||||
body: |
|
||||
## Changelog
|
||||
|
||||
##${{ env.CHANGELOG }}
|
||||
draft: false
|
||||
prerelease: ${{ endsWith(env.RELEASE_VERSION, '-beta') }}
|
185
API.md
@@ -88,7 +88,8 @@ Required parameters:
|
||||
section_id (str): The id of the Plex library section
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
server_id (str): The Plex server identifier of the library section
|
||||
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -103,7 +104,7 @@ Required parameters:
|
||||
user_id (str): The id of the Plex user
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -114,6 +115,21 @@ Returns:
|
||||
Delete and recreate the cache directory.
|
||||
|
||||
|
||||
### delete_history
|
||||
Delete history rows from Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
row_ids (str): Comma separated row ids to delete, e.g. "65,110,2,3645"
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### delete_hosted_images
|
||||
Delete the images uploaded to image hosting services.
|
||||
|
||||
@@ -146,7 +162,8 @@ Required parameters:
|
||||
section_id (str): The id of the Plex library section
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
server_id (str): The Plex server identifier of the library section
|
||||
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -173,10 +190,13 @@ Delete the 3rd party API lookup info.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
rating_key (int): 1234
|
||||
(Note: Must be the movie, show, artist, album, or track rating key)
|
||||
Optional parameters:
|
||||
None
|
||||
service (str): 'themoviedb' or 'tvmaze' or 'musicbrainz'
|
||||
delete_all (bool): 'true' to delete all images form the service
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -275,6 +295,10 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### delete_recently_added
|
||||
Flush out all of the recently added items in the database.
|
||||
|
||||
|
||||
### delete_temp_sessions
|
||||
Flush out all of the temporary sessions in the database.
|
||||
|
||||
@@ -287,7 +311,7 @@ Required parameters:
|
||||
user_id (str): The id of the Plex user
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -327,6 +351,7 @@ Required parameters:
|
||||
|
||||
Optional parameters:
|
||||
custom_thumb (str): The URL for the custom library thumbnail
|
||||
custom_art (str): The URL for the custom library background art
|
||||
keep_history (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
@@ -395,7 +420,11 @@ Returns:
|
||||
"banner": "/library/metadata/1219/banner/1503306930",
|
||||
"bif_thumb": "/library/parts/274169/indexes/sd/1000",
|
||||
"bitrate": "10617",
|
||||
"channel_call_sign": "",
|
||||
"channel_identifier": "",
|
||||
"channel_stream": 0,
|
||||
"channel_thumb": "",
|
||||
"children_count": "",
|
||||
"collections": [],
|
||||
"container": "mkv",
|
||||
"content_rating": "TV-MA",
|
||||
@@ -427,13 +456,15 @@ Returns:
|
||||
"ip_address": "10.10.10.1",
|
||||
"ip_address_public": "64.123.23.111",
|
||||
"is_admin": 1,
|
||||
"is_allow_sync": null,
|
||||
"is_allow_sync": 1,
|
||||
"is_home_user": 1,
|
||||
"is_restricted": 0,
|
||||
"keep_history": 1,
|
||||
"labels": [],
|
||||
"last_viewed_at": "1462165717",
|
||||
"library_name": "TV Shows",
|
||||
"live": 0,
|
||||
"live_uuid": "",
|
||||
"local": "1",
|
||||
"location": "lan",
|
||||
"machine_id": "lmd93nkn12k29j2lnm",
|
||||
@@ -442,8 +473,8 @@ Returns:
|
||||
"optimized_version": 0,
|
||||
"optimized_version_profile": "",
|
||||
"optimized_version_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"original_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
@@ -463,6 +494,7 @@ Returns:
|
||||
"rating_key": "153037",
|
||||
"relay": 0,
|
||||
"section_id": "2",
|
||||
"secure": 1,
|
||||
"session_id": "helf15l3rxgw01xxe0jf3l3d",
|
||||
"session_key": "27",
|
||||
"shared_libraries": [
|
||||
@@ -501,15 +533,21 @@ Returns:
|
||||
"stream_subtitle_location": "",
|
||||
"stream_video_bit_depth": "8",
|
||||
"stream_video_bitrate": "10233",
|
||||
"stream_video_chroma_subsampling": "4:2:0",
|
||||
"stream_video_codec": "h264",
|
||||
"stream_video_codec_level": "41",
|
||||
"stream_video_color_primaries": "",
|
||||
"stream_video_color_range": "tv",
|
||||
"stream_video_color_space": "bt709",
|
||||
"stream_video_color_trc": "",
|
||||
"stream_video_decision": "direct play",
|
||||
"stream_video_dynamic_range": "SDR",
|
||||
"stream_video_framerate": "24p",
|
||||
"stream_video_full_resolution": "1080p",
|
||||
"stream_video_height": "1078",
|
||||
"stream_video_language": "",
|
||||
"stream_video_language_code": "",
|
||||
"stream_video_ref_frames": "4",
|
||||
"stream_video_full_resolution": "1080p",
|
||||
"stream_video_resolution": "1080",
|
||||
"stream_video_scan_type": "progressive",
|
||||
"stream_video_width": "1920",
|
||||
@@ -559,9 +597,15 @@ Returns:
|
||||
"username": "LordCommanderSnow",
|
||||
"video_bit_depth": "8",
|
||||
"video_bitrate": "10233",
|
||||
"video_chroma_subsampling": "4:2:0",
|
||||
"video_codec": "h264",
|
||||
"video_codec_level": "41",
|
||||
"video_color_primaries": "",
|
||||
"video_color_range": "tv",
|
||||
"video_color_space": "bt709",
|
||||
"video_color_trc": ",
|
||||
"video_decision": "direct play",
|
||||
"video_dynamic_range": "SDR",
|
||||
"video_frame_rate": "23.976",
|
||||
"video_framerate": "24p",
|
||||
"video_full_resolution": "1080p",
|
||||
@@ -671,8 +715,9 @@ Optional parameters:
|
||||
grandparent_rating_key (int): 351
|
||||
start_date (str): "YYYY-MM-DD"
|
||||
section_id (int): 2
|
||||
media_type (str): "movie", "episode", "track"
|
||||
media_type (str): "movie", "episode", "track", "live"
|
||||
transcode_decision (str): "direct play", "copy", "transcode",
|
||||
guid (str): Plex guid for an item, e.g. "com.plexapp.agents.thetvdb://121361/6/1"
|
||||
order_column (str): "date", "friendly_name", "ip_address", "platform", "player",
|
||||
"full_title", "started", "paused_counter", "stopped", "duration"
|
||||
order_dir (str): "desc" or "asc"
|
||||
@@ -697,10 +742,12 @@ Returns:
|
||||
"original_title": "",
|
||||
"group_count": 1,
|
||||
"group_ids": "1124",
|
||||
"id": 1124,
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"ip_address": "xxx.xxx.xxx.xxx",
|
||||
"live": 0,
|
||||
"media_index": 17,
|
||||
"media_type": "episode",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_media_index": 7,
|
||||
"parent_rating_key": 544,
|
||||
"parent_title": "",
|
||||
@@ -711,6 +758,7 @@ Returns:
|
||||
"player": "Castle-PC",
|
||||
"rating_key": 4348,
|
||||
"reference_id": 1123,
|
||||
"row_id": 1124,
|
||||
"session_key": null,
|
||||
"started": 1462688107,
|
||||
"state": null,
|
||||
@@ -758,8 +806,10 @@ Returns:
|
||||
[{"content_rating": "TV-MA",
|
||||
"friendly_name": "",
|
||||
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"labels": [],
|
||||
"last_play": 1462380698,
|
||||
"live": 0,
|
||||
"media_type": "episode",
|
||||
"platform": "",
|
||||
"platform_type": "",
|
||||
@@ -820,6 +870,7 @@ Returns:
|
||||
[{"art": "/:/resources/show-fanart.jpg",
|
||||
"child_count": "3745",
|
||||
"count": "62",
|
||||
"is_active": 1,
|
||||
"parent_count": "240",
|
||||
"section_id": "2",
|
||||
"section_name": "TV Shows",
|
||||
@@ -860,23 +911,29 @@ Returns:
|
||||
"do_notify": "Checked",
|
||||
"do_notify_created": "Checked",
|
||||
"duration": 1578037,
|
||||
"id": 1128,
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"histroy_row_id": 1128,
|
||||
"is_active": 1,
|
||||
"keep_history": "Checked",
|
||||
"labels": [],
|
||||
"last_accessed": 1462693216,
|
||||
"last_played": "Game of Thrones - The Red Woman",
|
||||
"library_art": "/:/resources/show-fanart.jpg",
|
||||
"library_thumb": "",
|
||||
"library_thumb": "/:/resources/show.png",
|
||||
"live": 0,
|
||||
"media_index": 1,
|
||||
"media_type": "episode",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_count": 240,
|
||||
"parent_media_index": 6,
|
||||
"parent_title": "",
|
||||
"plays": 772,
|
||||
"rating_key": 153037,
|
||||
"row_id": 1,
|
||||
"section_id": 2,
|
||||
"section_name": "TV Shows",
|
||||
"section_type": "Show",
|
||||
"server_id": "ds48g4r354a8v9byrrtr697g3g79w",
|
||||
"thumb": "/library/metadata/153036/thumb/1462175062",
|
||||
"year": 2016
|
||||
},
|
||||
@@ -904,13 +961,16 @@ Returns:
|
||||
"deleted_section": 0,
|
||||
"do_notify": 1,
|
||||
"do_notify_created": 1,
|
||||
"is_active": 1,
|
||||
"keep_history": 1,
|
||||
"library_art": "/:/resources/movie-fanart.jpg",
|
||||
"library_thumb": "/:/resources/movie.png",
|
||||
"parent_count": null,
|
||||
"row_id": 1,
|
||||
"section_id": 1,
|
||||
"section_name": "Movies",
|
||||
"section_type": "movie"
|
||||
"section_type": "movie",
|
||||
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -958,6 +1018,7 @@ Returns:
|
||||
"rating_key": "1219",
|
||||
"section_id": 2,
|
||||
"section_type": "show",
|
||||
"sort_title": "Game of Thrones",
|
||||
"thumb": "/library/metadata/1219/thumb/1436265995",
|
||||
"title": "Game of Thrones",
|
||||
"video_codec": "",
|
||||
@@ -1025,10 +1086,11 @@ Get a library's watch time statistics.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
section_id (str): The id of the Plex library section
|
||||
section_id (str): The id of the Plex library section
|
||||
|
||||
Optional parameters:
|
||||
grouping (int): 0 or 1
|
||||
query_days (str): Comma separated days, e.g. "1,7,30,0"
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -1124,6 +1186,7 @@ Returns:
|
||||
"labels": [],
|
||||
"last_viewed_at": "1462165717",
|
||||
"library_name": "TV Shows",
|
||||
"live": 0,
|
||||
"media_index": "1",
|
||||
"media_info": [
|
||||
{
|
||||
@@ -1133,6 +1196,9 @@ Returns:
|
||||
"audio_codec": "ac3",
|
||||
"audio_profile": "",
|
||||
"bitrate": "10617",
|
||||
"channel_call_sign": "",
|
||||
"channel_identifier": "",
|
||||
"channel_thumb": "",
|
||||
"container": "mkv",
|
||||
"height": "1078",
|
||||
"id": "257925",
|
||||
@@ -1151,6 +1217,10 @@ Returns:
|
||||
"video_bitrate": "10233",
|
||||
"video_codec": "h264",
|
||||
"video_codec_level": "41",
|
||||
"video_color_primaries": "",
|
||||
"video_color_range": "tv",
|
||||
"video_color_space": "bt709",
|
||||
"video_color_trc": "",
|
||||
"video_frame_rate": "23.976",
|
||||
"video_height": "1078",
|
||||
"video_language": "",
|
||||
@@ -1210,7 +1280,7 @@ Returns:
|
||||
"rating_image": "rottentomatoes://image.rating.ripe",
|
||||
"rating_key": "153037",
|
||||
"section_id": "2",
|
||||
"sort_title": "Game of Thrones",
|
||||
"sort_title": "Red Woman",
|
||||
"studio": "HBO",
|
||||
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
|
||||
"tagline": "",
|
||||
@@ -1506,7 +1576,8 @@ Returns:
|
||||
"series":
|
||||
[{"name": "Movies", "data": [...]}
|
||||
{"name": "TV", "data": [...]},
|
||||
{"name": "Music", "data": [...]}
|
||||
{"name": "Music", "data": [...]},
|
||||
{"name": "Live TV", "data": [...]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1532,7 +1603,8 @@ Returns:
|
||||
"series":
|
||||
[{"name": "Movies", "data": [...]}
|
||||
{"name": "TV", "data": [...]},
|
||||
{"name": "Music", "data": [...]}
|
||||
{"name": "Music", "data": [...]},
|
||||
{"name": "Live TV", "data": [...]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1558,7 +1630,8 @@ Returns:
|
||||
"series":
|
||||
[{"name": "Movies", "data": [...]}
|
||||
{"name": "TV", "data": [...]},
|
||||
{"name": "Music", "data": [...]}
|
||||
{"name": "Music", "data": [...]},
|
||||
{"name": "Live TV", "data": [...]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1662,7 +1735,8 @@ Returns:
|
||||
"series":
|
||||
[{"name": "Movies", "data": [...]}
|
||||
{"name": "TV", "data": [...]},
|
||||
{"name": "Music", "data": [...]}
|
||||
{"name": "Music", "data": [...]},
|
||||
{"name": "Live TV", "data": [...]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1688,7 +1762,8 @@ Returns:
|
||||
"series":
|
||||
[{"name": "Movies", "data": [...]}
|
||||
{"name": "TV", "data": [...]},
|
||||
{"name": "Music", "data": [...]}
|
||||
{"name": "Music", "data": [...]},
|
||||
{"name": "Live TV", "data": [...]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1714,7 +1789,8 @@ Returns:
|
||||
"series":
|
||||
[{"name": "Movies", "data": [...]}
|
||||
{"name": "TV", "data": [...]},
|
||||
{"name": "Music", "data": [...]}
|
||||
{"name": "Music", "data": [...]},
|
||||
{"name": "Live TV", "data": [...]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1802,22 +1878,59 @@ Optional parameters:
|
||||
Returns:
|
||||
json:
|
||||
{"recently_added":
|
||||
[{"added_at": "1461572396",
|
||||
[{"actors": [
|
||||
"Kit Harington",
|
||||
"Emilia Clarke",
|
||||
"Isaac Hempstead-Wright",
|
||||
"Maisie Williams",
|
||||
"Liam Cunningham",
|
||||
],
|
||||
"added_at": "1461572396",
|
||||
"art": "/library/metadata/1219/art/1462175063",
|
||||
"audience_rating": "8",
|
||||
"audience_rating_image": "rottentomatoes://image.rating.upright",
|
||||
"banner": "/library/metadata/1219/banner/1462175063",
|
||||
"directors": [
|
||||
"Jeremy Podeswa"
|
||||
],
|
||||
"duration": "2998290",
|
||||
"full_title": "Game of Thrones - The Red Woman",
|
||||
"genres": [
|
||||
"Adventure",
|
||||
"Drama",
|
||||
"Fantasy"
|
||||
],
|
||||
"grandparent_rating_key": "1219",
|
||||
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
|
||||
"grandparent_title": "Game of Thrones",
|
||||
"library_name": "",
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"labels": [],
|
||||
"last_viewed_at": "1462165717",
|
||||
"library_name": "TV Shows",
|
||||
"media_index": "1",
|
||||
"media_type": "episode",
|
||||
"original_title": "",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_media_index": "6",
|
||||
"parent_rating_key": "153036",
|
||||
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
||||
"parent_title": "",
|
||||
"rating": "7.8",
|
||||
"rating_image": "rottentomatoes://image.rating.ripe",
|
||||
"rating_key": "153037",
|
||||
"section_id": "2",
|
||||
"sort_title": "Red Woman",
|
||||
"studio": "HBO",
|
||||
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
|
||||
"tagline": "",
|
||||
"thumb": "/library/metadata/153037/thumb/1462175060",
|
||||
"title": "The Red Woman",
|
||||
"user_rating": "9.0",
|
||||
"updated_at": "1462175060",
|
||||
"writers": [
|
||||
"David Benioff",
|
||||
"D. B. Weiss"
|
||||
],
|
||||
"year": "2016"
|
||||
},
|
||||
{...},
|
||||
@@ -1999,6 +2112,7 @@ Returns:
|
||||
"stream_video_bitrate": 527,
|
||||
"stream_video_codec": "h264",
|
||||
"stream_video_decision": "transcode",
|
||||
"stream_video_dynamic_range": "SDR",
|
||||
"stream_video_framerate": "24p",
|
||||
"stream_video_height": 306,
|
||||
"stream_video_resolution": "SD",
|
||||
@@ -2013,6 +2127,7 @@ Returns:
|
||||
"video_bitrate": 2500,
|
||||
"video_codec": "h264",
|
||||
"video_decision": "transcode",
|
||||
"video_dynamic_range": "SDR",
|
||||
"video_framerate": "24p",
|
||||
"video_height": 816,
|
||||
"video_resolution": "1080",
|
||||
@@ -2132,10 +2247,13 @@ Returns:
|
||||
"do_notify": 1,
|
||||
"email": "Jon.Snow.1337@CastleBlack.com",
|
||||
"friendly_name": "Jon Snow",
|
||||
"is_active": 1,
|
||||
"is_admin": 0,
|
||||
"is_allow_sync": 1,
|
||||
"is_home_user": 1,
|
||||
"is_restricted": 0,
|
||||
"keep_history": 1,
|
||||
"row_id": 1,
|
||||
"shared_libraries": ["10", "1", "4", "5", "15", "20", "2"],
|
||||
"user_id": 133788,
|
||||
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
|
||||
@@ -2166,12 +2284,15 @@ Returns:
|
||||
"recordsFiltered": 10,
|
||||
"data":
|
||||
[{"friendly_name": "Jon Snow",
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"id": 1121,
|
||||
"ip_address": "xxx.xxx.xxx.xxx",
|
||||
"last_played": "Game of Thrones - The Red Woman",
|
||||
"last_seen": 1462591869,
|
||||
"live": 0,
|
||||
"media_index": 1,
|
||||
"media_type": "episode",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_media_index": 6,
|
||||
"parent_title": "",
|
||||
"platform": "Chrome",
|
||||
@@ -2285,6 +2406,7 @@ Required parameters:
|
||||
|
||||
Optional parameters:
|
||||
grouping (int): 0 or 1
|
||||
query_days (str): Comma separated days, e.g. "1,7,30,0"
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -2328,11 +2450,13 @@ Returns:
|
||||
"filter_music": "",
|
||||
"filter_photos": "",
|
||||
"filter_tv": "",
|
||||
"is_active": 1,
|
||||
"is_admin": 0,
|
||||
"is_allow_sync": 1,
|
||||
"is_home_user": 1,
|
||||
"is_restricted": 0,
|
||||
"keep_history": 1,
|
||||
"row_id": 1,
|
||||
"server_token": "PU9cMuQZxJKFBtGqHk68",
|
||||
"shared_libraries": "1;2;3",
|
||||
"thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
|
||||
@@ -2371,19 +2495,24 @@ Returns:
|
||||
"do_notify": "Checked",
|
||||
"duration": 2998290,
|
||||
"friendly_name": "Jon Snow",
|
||||
"id": 1121,
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"history_row_id": 1121,
|
||||
"ip_address": "xxx.xxx.xxx.xxx",
|
||||
"is_active": 1,
|
||||
"keep_history": "Checked",
|
||||
"last_played": "Game of Thrones - The Red Woman",
|
||||
"last_seen": 1462591869,
|
||||
"live": 0,
|
||||
"media_index": 1,
|
||||
"media_type": "episode",
|
||||
"originally_available_at": "2016-04-24",
|
||||
"parent_media_index": 6,
|
||||
"parent_title": "",
|
||||
"platform": "Chrome",
|
||||
"player": "Plex Web (Chrome)",
|
||||
"plays": 487,
|
||||
"rating_key": 153037,
|
||||
"row_id": 1,
|
||||
"thumb": "/library/metadata/153036/thumb/1462175062",
|
||||
"transcode_decision": "transcode",
|
||||
"user_id": 133788,
|
||||
@@ -2516,10 +2645,10 @@ Optional parameters:
|
||||
width (str): 300
|
||||
height (str): 450
|
||||
opacity (str): 25
|
||||
background (str): 282828
|
||||
background (str): Hex color, e.g. 282828
|
||||
blur (str): 3
|
||||
img_format (str): png
|
||||
fallback (str): "poster", "cover", "art"
|
||||
fallback (str): "poster", "cover", "art", "poster-live", "art-live", "art-live-full"
|
||||
refresh (bool): True or False whether to refresh the image cache
|
||||
return_hash (bool): True or False to return the self-hosted image hash instead of the image
|
||||
|
||||
@@ -2645,7 +2774,7 @@ Returns:
|
||||
### sql
|
||||
Query the Tautulli database with raw SQL. Automatically makes a backup of
|
||||
the database if the latest backup is older then 24h. `api_sql` must be
|
||||
manually enabled in the config file.
|
||||
manually enabled in the config file while Tautulli is shut down.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
|
123
CHANGELOG.md
@@ -1,5 +1,128 @@
|
||||
# Changelog
|
||||
|
||||
## v2.2.2-beta (2020-04-12)
|
||||
|
||||
* Notifications:
|
||||
* New: Added notification trigger for Tautulli database corruption.
|
||||
* New: Added TAUTULLI_PYTHON_VERSION to script notification environment variables.
|
||||
* Fix: Notification grouping by season/album and show/artist not enabled by default.
|
||||
* Change: The file size notification parameter is now reported in SI units. (Thanks @aaronldunlap)
|
||||
* UI:
|
||||
* Fix: Delete lookup info from the media info page failing.
|
||||
* New: Added icon on the users table to indicate if the user is not on the Plex server.
|
||||
* New: Added icon on the libraries table to indicate if the library is not on the Plex server.
|
||||
* Fix: XBMC platform icon not being redirected to the Kodi platform icon.
|
||||
* Change: Improved deleting libraries so libraries with the same section ID are not also deleted.
|
||||
* API:
|
||||
* Fix: Returning XML for the API failing due to unicode characters.
|
||||
* Fix: Grouping parameter for various API commands not falling back to default setting.
|
||||
* New: Added time_queries parameter to get_library_watch_time_stats and get_user_watch_time_stats API command. (Thanks @KaasKop97)
|
||||
* New: Added an "is_active" return value to the get_user, get_users, get_library, and get_libraries API commands which indicates if the user or library is on the Plex server.
|
||||
* New: Added delete_history API command.
|
||||
* Change: Added optional parameter for row_ids for delete_library, delete_user, delete_all_library_history, and delete_all_user_history API commands.
|
||||
* Mobile App:
|
||||
* Fix: Temporary device token not being invalidated after cancelling device registration.
|
||||
* Other:
|
||||
* Fix: Update failing on CentOS due to an older git version.
|
||||
* Fix: Manifest file for creating a web app had incorrect info.
|
||||
* New: Docker images updated to support ARM platforms.
|
||||
|
||||
|
||||
## v2.2.1 (2020-03-28)
|
||||
|
||||
* Notifications:
|
||||
* Fix: File size notification parameter incorrectly truncated to an integer.
|
||||
* Fix: Notification grouping by season/album not enabled by default.
|
||||
* New: Added transcode decision counts to notification parameters.
|
||||
* Change: Tags (<>) are no longer stripped from from Webhook notification text.
|
||||
* Newsletter:
|
||||
* New: Added favicon to newsletter template when viewing as a web page.
|
||||
* UI:
|
||||
* Fix: Username missing from the Synced Items table.
|
||||
* Fix: Windows system tray icon not enabled by default.
|
||||
* Fix: Saving a mobile device with a blank friendly name caused an error.
|
||||
* New: Added IMDb and Rotten Tomato Ratings to info pages.
|
||||
* New: Added button in settings to delete all 3rd party metadata lookup info in the database.
|
||||
* New: Added button in settings to flush recently added items in the database.
|
||||
* API:
|
||||
* New: Added delete_recenly_added API command to flush recently added items.
|
||||
* Change: Updated delete_lookup_info API command parameters to allow deleteing all 3rd party metadata lookup info.
|
||||
|
||||
|
||||
## v2.2.0 (2020-03-08)
|
||||
|
||||
* Important Note!
|
||||
* All Live TV changes requires Plex Media Server 1.18.7 or higher.
|
||||
* Monitoring:
|
||||
* Fix: Improved IPv6 display on the activity cards. (Thanks @felixbuenemann)
|
||||
* New: Added Live TV metadata and posters to the activity cards.
|
||||
* Change: Show bandwidth in Gbps when greater than 1000 Mbps.
|
||||
* History:
|
||||
* New: Added history logging for Live TV sessions.
|
||||
* New: Added a fake "Live TV" library to collect Live TV history.
|
||||
* Note: This library will show up the first time that Live TV is played.
|
||||
* New: Added the ability to filter history by Live TV.
|
||||
* Graphs:
|
||||
* New: Added Live TV series to the "Plays by Period" and "Play Totals" graphs.
|
||||
* Change: Media type series on the graphs are only shown if the corresponding library type is present.
|
||||
* Notifications:
|
||||
* Fix: Race condition causing stream count to be incorrect for playback stop notifications.
|
||||
* New: Added Live TV channel notification parameters.
|
||||
* New: Added Plex background art notification parameter.
|
||||
* Note: This is the Plex API endpoint to retrieve the background art, not the actual image.
|
||||
* New: Added poster images for clip notifications.
|
||||
* Change: Default Webhook notification method to POST.
|
||||
* UI:
|
||||
* Fix: Windows platform showing up twice on the Most Active Platforms statistics card.
|
||||
* New: Added option to change the background art for library sections when editing a library.
|
||||
* New: Added button to reset Tautulli git installation in settings to fix failed git updates.
|
||||
* API:
|
||||
* New: Added ability to filter history using a "live" media type and by guid for the get_history API command.
|
||||
* New: Added cutsom_art parameter to the edit_library API command.
|
||||
* Other:
|
||||
* Change: Add crossorigin use-credentials attribute to manifest tags. (Thanks @pkoenig10)
|
||||
* Change: Disable automatic updates for Docker containers. Updates are now handled by updating the Docker container.
|
||||
* Note: If you are using an old Docker container created before v2.2.0, then you may need to completely remove and recreate the container to update for the first time.
|
||||
* Note: Use the ":latest" Docker tag for the newest stable release, or the ":beta" or ":nightly" tags to access the beta or nightly branches.
|
||||
|
||||
|
||||
## v2.1.44 (2020-02-05)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: SDR source video being identified as HDR stream video.
|
||||
* Notifications:
|
||||
* Fix: Unable to select condition operator for video color parameters.
|
||||
* UI:
|
||||
* Fix: Capitalization for platforms on history tables.
|
||||
|
||||
|
||||
## v2.1.43 (2020-02-03)
|
||||
|
||||
* Monitoring:
|
||||
* New: Added HDR indicator on activity card.
|
||||
* New: Added dynamic range to history steam info modal.
|
||||
* Notifications:
|
||||
* Fix: Webhook notification body sent as incorrect data type when Content-Type header is overridden.
|
||||
* Fix: Telegram notification character limit incorrect for unicode characters.
|
||||
* New: Added color and dynamic range notification parameters.
|
||||
* Newsletters:
|
||||
* Fix: Episodes and Albums plural spelling on recently added newsletter section headers.
|
||||
* UI:
|
||||
* Fix: Windows and macOS platform capitalization.
|
||||
* Fix: Season number 0 not shown for episodes on history tables.
|
||||
* Other:
|
||||
* Change: Mask email addresses in logs.
|
||||
* Change: Update deprecated GitHub access token URL parameter to Authorization header.
|
||||
|
||||
|
||||
## v2.1.42 (2020-01-04)
|
||||
|
||||
* Other:
|
||||
* Fix: SSL certificate error when installing GeoLite2 database.
|
||||
* Change: Verify MaxMind license key and GeoLite2 database path before installing.
|
||||
* Change: Disable GeoLite2 database uninstall button when it is not installed.
|
||||
|
||||
|
||||
## v2.1.41 (2019-12-30)
|
||||
|
||||
* Other:
|
||||
|
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM tautulli/tautulli-baseimage:latest
|
||||
|
||||
LABEL maintainer="Tautulli"
|
||||
|
||||
ARG BRANCH
|
||||
ARG COMMIT
|
||||
|
||||
ENV TAUTULLI_DOCKER=True
|
||||
ENV TZ=UTC
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN \
|
||||
echo ${BRANCH} > /app/branch.txt && \
|
||||
echo ${COMMIT} > /app/version.txt
|
||||
|
||||
COPY . /app
|
||||
|
||||
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
||||
|
||||
VOLUME /config /plex_logs
|
||||
EXPOSE 8181
|
||||
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1
|
25
README.md
@@ -1,9 +1,5 @@
|
||||
# Tautulli
|
||||
|
||||
[](https://tautulli.com/discord)
|
||||
[](https://www.reddit.com/r/Tautulli/)
|
||||
[](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242)
|
||||
|
||||
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
|
||||
|
||||
This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).
|
||||
@@ -31,7 +27,21 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
||||
|
||||

|
||||
|
||||
## Installation and Support
|
||||
## Installation & Support
|
||||
|
||||
[](https://python.org/downloads/release/python-2717/)
|
||||
[](https://hub.docker.com/r/tautulli/tautulli)
|
||||
[](https://hub.docker.com/r/tautulli/tautulli)
|
||||
|
||||
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
|
||||
| --- | --- | --- | --- |
|
||||
| Release | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/commits/beta) | [](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [](https://github.com/Tautulli/Tautulli/commits/nightly) |
|
||||
| Docker | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli-Wiki/wiki)
|
||||
[](https://tautulli.com/discord)
|
||||
[](https://www.reddit.com/r/Tautulli/)
|
||||
[](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242)
|
||||
|
||||
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
|
||||
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
|
||||
@@ -39,10 +49,15 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
||||
|
||||
## Issues & Feature Requests
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli-Issues)
|
||||
[](https://feathub.com/Tautulli/Tautulli)
|
||||
|
||||
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
|
||||
|
||||
## License
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli/blob/master/LICENSE)
|
||||
|
||||
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
|
||||
|
||||
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
|
10
Tautulli.py
@@ -36,7 +36,7 @@ import time
|
||||
import tzlocal
|
||||
|
||||
import plexpy
|
||||
from plexpy import config, database, logger, webstart
|
||||
from plexpy import config, database, helpers, logger, webstart
|
||||
|
||||
|
||||
# Register signals, such as CTRL + C
|
||||
@@ -106,8 +106,8 @@ def main():
|
||||
plexpy.QUIET = True
|
||||
|
||||
# Do an intial setup of the logger.
|
||||
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
|
||||
verbose=plexpy.VERBOSE)
|
||||
# Require verbose for pre-initilization to see critical errors
|
||||
logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)
|
||||
|
||||
try:
|
||||
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
|
||||
@@ -117,7 +117,7 @@ def main():
|
||||
|
||||
plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z')
|
||||
|
||||
if os.getenv('TAUTULLI_DOCKER', False) == 'True':
|
||||
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
||||
plexpy.DOCKER = True
|
||||
|
||||
if args.dev:
|
||||
@@ -263,6 +263,8 @@ def main():
|
||||
plexpy.shutdown(restart=True)
|
||||
elif plexpy.SIGNAL == 'checkout':
|
||||
plexpy.shutdown(restart=True, checkout=True)
|
||||
elif plexpy.SIGNAL == 'reset':
|
||||
plexpy.shutdown(restart=True, reset=True)
|
||||
else:
|
||||
plexpy.shutdown(restart=True, update=True)
|
||||
|
||||
|
@@ -28,7 +28,7 @@
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
@@ -43,23 +43,23 @@
|
||||
<div class="container">
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
% if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is None:
|
||||
% if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is not False:
|
||||
<div id="updatebar" style="display: none;">
|
||||
% if plexpy.UPDATE_AVAILABLE is None:
|
||||
You are running an unknown version of Tautulli.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
</div>
|
||||
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE == 'release':
|
||||
<div id="updatebar" style="display: none;">
|
||||
% elif plexpy.UPDATE_AVAILABLE == 'release':
|
||||
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
|
||||
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
</div>
|
||||
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE == 'commit':
|
||||
<div id="updatebar" style="display: none;">
|
||||
% elif plexpy.UPDATE_AVAILABLE == 'commit':
|
||||
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
||||
newer version</a> of Tautulli is available!<br />
|
||||
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
|
||||
% endif
|
||||
% if plexpy.DOCKER:
|
||||
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% else:
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% endif
|
||||
</div>
|
||||
% else:
|
||||
<div id="updatebar" style="display: none;"></div>
|
||||
@@ -291,6 +291,7 @@ ${next.modalIncludes()}
|
||||
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
|
||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||
<script src="${http_root}js/platform.min.js"></script>
|
||||
<script src="${http_root}js/ipaddr.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||
@@ -318,21 +319,23 @@ ${next.modalIncludes()}
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = '';
|
||||
if (result.update === null) {
|
||||
msg = 'You are running an unknown version of Tautulli.<br />' +
|
||||
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
$('#updatebar').html(msg).fadeIn();
|
||||
} else if (result.update === true && result.release === true) {
|
||||
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />' +
|
||||
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
$('#updatebar').html(msg).fadeIn();
|
||||
} else if (result.update === true && result.release === false) {
|
||||
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
|
||||
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />' +
|
||||
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
$('#updatebar').html(msg).fadeIn();
|
||||
} else if (result.update === false) {
|
||||
if (result.update === false) {
|
||||
showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
|
||||
} else {
|
||||
if (result.update === null) {
|
||||
msg = 'You are running an unknown version of Tautulli.<br />';
|
||||
} else if (result.update === true && result.release === true) {
|
||||
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />';
|
||||
} else if (result.update === true && result.release === false) {
|
||||
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
|
||||
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />';
|
||||
}
|
||||
if (result.docker) {
|
||||
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else {
|
||||
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
}
|
||||
$('#updatebar').html(msg).fadeIn();
|
||||
}
|
||||
|
||||
if (_callback) {
|
||||
|
@@ -21,7 +21,7 @@ ul.ColVis_collection li {
|
||||
|
||||
.ColVis_Button:hover,
|
||||
ul.ColVis_collection li:hover {
|
||||
color: #F9AA03;
|
||||
color: #E5A00D;
|
||||
}
|
||||
|
||||
button.ColVis_Button {
|
||||
|
@@ -101,7 +101,7 @@ table.display tr:hover a {
|
||||
text-decoration:none;
|
||||
}
|
||||
table.display td:hover a {
|
||||
color: #F9AA03;
|
||||
color: #E5A00D;
|
||||
}
|
||||
table.display thead tr:hover {
|
||||
background-color: #212121;
|
||||
|
@@ -523,7 +523,7 @@ fieldset[disabled] .btn-bright.active {
|
||||
color: #eee;
|
||||
}
|
||||
.modal-body i {
|
||||
color: #F9AA03;
|
||||
color: #E5A00D;
|
||||
}
|
||||
.modal-body i.fa {
|
||||
color: #fff;
|
||||
@@ -534,7 +534,7 @@ fieldset[disabled] .btn-bright.active {
|
||||
}
|
||||
.modal-body strong,
|
||||
.modal-body strong i.fa {
|
||||
color: #F9AA03;
|
||||
color: #E5A00D;
|
||||
}
|
||||
.modal-footer {
|
||||
padding: 15px 20px;
|
||||
@@ -673,7 +673,7 @@ textarea.form-control:focus {
|
||||
color: #fff;
|
||||
}
|
||||
.form-control-feedback {
|
||||
color: #F9AA03;
|
||||
color: #E5A00D;
|
||||
margin: 5px 40px 5px 0;
|
||||
}
|
||||
.form-control[disabled],
|
||||
@@ -711,7 +711,6 @@ fieldset[disabled] .form-control {
|
||||
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
}
|
||||
.users-poster-face {
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
@@ -857,7 +856,6 @@ a .users-poster-face:hover {
|
||||
z-index: 2;
|
||||
}
|
||||
.dashboard-activity-info-platform {
|
||||
padding: 6px !important;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
width: 50px;
|
||||
@@ -973,7 +971,7 @@ a .users-poster-face:hover {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
max-width: 125px;
|
||||
}
|
||||
.dashboard-activity-info-time {
|
||||
position: absolute;
|
||||
@@ -1036,13 +1034,13 @@ a .users-poster-face:hover {
|
||||
}
|
||||
.dashboard-activity-container:hover .progress-bar {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25),0%,rgba(0,0,0,0),50px);
|
||||
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
|
||||
background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
|
||||
background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
|
||||
}
|
||||
.dashboard-activity-container:hover .buffer-bar {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25),0%,rgba(0,0,0,0),50px);
|
||||
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
|
||||
background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
|
||||
background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
|
||||
}
|
||||
@@ -1742,7 +1740,7 @@ a:hover .dashboard-recent-media-cover {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-image: -webkit-gradient(linear,left 0,left 100%,from(rgba(0,0,0,.7)),to(rgba(0,0,0,.9)));
|
||||
background-image: -webkit-linear-gradient(top,rgba(0,0,0,.7),0,rgba(0,0,0,.9),100%);
|
||||
background-image: -webkit-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||
background-repeat: repeat-x;
|
||||
@@ -1921,6 +1919,16 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
|
||||
margin-left: 2px;
|
||||
color: #999;
|
||||
}
|
||||
.critic-rating {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-top: 2px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
float: right;
|
||||
}
|
||||
.children-list,
|
||||
.search-results-list {
|
||||
position: relative;
|
||||
@@ -2177,7 +2185,7 @@ li.advanced-setting {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
padding-top: 27px;
|
||||
padding-left: 110px;
|
||||
padding-left: 105px;
|
||||
}
|
||||
.user-info-nav {
|
||||
margin-top: 15px;
|
||||
@@ -3102,12 +3110,28 @@ div.dataTables_info {
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.tooltip-inner {
|
||||
max-width: 250px;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
border: 0;
|
||||
font-weight: bold;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.inactive-library-tooltip,
|
||||
.inactive-user-tooltip {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.inactive-library-tooltip i.fa,
|
||||
.inactive-user-tooltip i.fa {
|
||||
color: #E5A00D;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-shadow: 0 0 2px rgba(0,0,0,.5);
|
||||
}
|
||||
.history-thumbnail-popover {
|
||||
z-index: 2000;
|
||||
padding: 0;
|
||||
@@ -3134,6 +3158,37 @@ div.dataTables_info {
|
||||
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
}
|
||||
.channel-thumbnail-popover {
|
||||
z-index: 2000;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.channel-thumbnail-popover.popover.left {
|
||||
margin-left: -15px;
|
||||
}
|
||||
.channel-thumbnail-popover.popover.right {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.channel-thumbnail-popover .popover-content {
|
||||
color: #000;
|
||||
padding: 0;
|
||||
}
|
||||
.channel-thumbnail {
|
||||
background-color: #868b8b;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-origin: content-box;
|
||||
background-repeat: no-repeat;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
padding: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.channel-thumbnail-popover .arrow:after {
|
||||
border-right-color: #868b8b !important;
|
||||
}
|
||||
.edit-user-toggles,
|
||||
.edit-library-toggles {
|
||||
padding-right: 10px;
|
||||
@@ -3766,9 +3821,8 @@ a:hover .overlay-refresh-image:hover {
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
padding: 10px;
|
||||
background-size: calc(100% - 20px) calc(100% - 20px) !important;
|
||||
background-origin: content-box !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
@@ -3878,7 +3932,7 @@ a:hover .overlay-refresh-image:hover {
|
||||
}
|
||||
.platform-xbmc {
|
||||
background-color: #3b4872;
|
||||
background-image: url(../images/platforms/xbmc.svg);
|
||||
background-image: url(../images/platforms/kodi.svg);
|
||||
}
|
||||
.platform-xbox {
|
||||
background-color: #107c10;
|
||||
@@ -3983,10 +4037,39 @@ a:hover .overlay-refresh-image:hover {
|
||||
.library-video {
|
||||
background-image: url(../images/libraries/video.svg);
|
||||
}
|
||||
.library-live {
|
||||
background-image: url(../images/libraries/live.svg);
|
||||
}
|
||||
.stats-most_concurrent {
|
||||
background-image: url(../images/icons/most-concurrent-streams.svg);
|
||||
}
|
||||
|
||||
.rating-image {
|
||||
width: 51px;
|
||||
height: 20px;
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
background-origin: content-box !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: left !important;
|
||||
text-align: right;
|
||||
}
|
||||
.rating-imdb {
|
||||
width: 62px !important;
|
||||
background-image: url(../images/rating/imdb.svg);
|
||||
}
|
||||
.rating-rottentomatos-ripe {
|
||||
background-image: url(../images/rating/tomato-ripe.svg);
|
||||
}
|
||||
.rating-rottentomatos-rotten {
|
||||
background-image: url(../images/rating/tomato-rotten.svg);
|
||||
}
|
||||
.rating-rottentomatos-upright {
|
||||
background-image: url(../images/rating/popcorn-upright.svg);
|
||||
}
|
||||
.rating-rottentomatos-spilled {
|
||||
background-image: url(../images/rating/popcorn-spilled.svg);
|
||||
}
|
||||
.transparent {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
@@ -4033,7 +4116,7 @@ a:hover .overlay-refresh-image:hover {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.stream-info .heading {
|
||||
color: #F9AA03;
|
||||
color: #E5A00D;
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-weight: bold !important;
|
||||
|
@@ -62,8 +62,7 @@ DOCUMENTATION :: END
|
||||
% if session is not None:
|
||||
<%
|
||||
from collections import defaultdict
|
||||
from urllib import quote
|
||||
from plexpy import helpers
|
||||
from plexpy.helpers import cast_to_int, page
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES
|
||||
import plexpy
|
||||
%>
|
||||
@@ -71,62 +70,67 @@ DOCUMENTATION :: END
|
||||
data = defaultdict(lambda: 'Unknown', **session)
|
||||
sk = data['session_key']
|
||||
|
||||
href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#'
|
||||
parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#'
|
||||
grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#'
|
||||
user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#'
|
||||
href = page('info', data['rating_key'])
|
||||
parent_href = page('info', data['parent_rating_key'])
|
||||
grandparent_href = page('info', data['grandparent_rating_key'])
|
||||
user_href = page('user', data['user_id']) if data['user_id'] else '#'
|
||||
%>
|
||||
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
|
||||
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
|
||||
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}"
|
||||
data-guid="${data['guid']}">
|
||||
<div class="dashboard-activity-container">
|
||||
<%
|
||||
if data['live'] == 1:
|
||||
background_url = 'images/art-live.png'
|
||||
if data['live']:
|
||||
background_url = page('pms_image_proxy', data['art'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art-live', refresh=True)
|
||||
elif data['channel_stream'] == 0:
|
||||
background_url = 'pms_image_proxy?img=' + data['art'] + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true'
|
||||
background_url = page('pms_image_proxy', data['art'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art', refresh=True)
|
||||
else:
|
||||
if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')):
|
||||
background_url = data['art']
|
||||
else:
|
||||
background_url = 'pms_image_proxy?img=' + quote(data['art'] or data['thumb']) + '&width=500&height=280&fallback=art&refresh=true&clip=true'
|
||||
background_url = page('pms_image_proxy', data['art'] or data['thumb'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art', refresh=True, clip=True)
|
||||
%>
|
||||
<div id="background-${sk}" class="dashboard-activity-background" style="background-image: url(${background_url});">
|
||||
<div class="dashboard-activity-poster-container hidden-xs">
|
||||
% if data['media_type'] == 'track':
|
||||
<div id="poster-${sk}-bg" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover&refresh=true);"></div>
|
||||
<div id="poster-${sk}-bg" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 300, 60, '282828', 3, fallback='cover', refresh=True)});"></div>
|
||||
% endif
|
||||
% if data['live'] == 1:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster-live.png);"></div>
|
||||
% if data['live']:
|
||||
% if data['media_type'] == 'movie':
|
||||
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster-live', refresh=True)});"></div>
|
||||
</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a id="poster-url-${sk}" href="${href}" title="${data['grandparent_title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'], data['rating_key'], 300, 450, fallback='poster-live', refresh=True)});"></div>
|
||||
</a>
|
||||
% else:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live', refresh=True)});"></div>
|
||||
% endif
|
||||
% elif data['channel_stream'] == 0:
|
||||
% if data['media_type'] == 'movie':
|
||||
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
|
||||
</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a id="poster-url-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'], data['grandparent_rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
|
||||
</a>
|
||||
% elif data['media_type'] == 'track':
|
||||
<a id="poster-url-${sk}" href="${parent_href}" title="${data['parent_title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 300, fallback='cover', refresh=True)});"></div>
|
||||
</a>
|
||||
% elif data['media_type'] in ('photo', 'clip'):
|
||||
% if data['extra_type']:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['art'].replace('/art', '/thumb') or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['art'].replace('/art', '/thumb') or data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
|
||||
% elif data['parent_thumb']:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
|
||||
% else:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb'] or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
|
||||
% endif
|
||||
% else:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster.png);"></div>
|
||||
% endif
|
||||
% else:
|
||||
% if data['channel_icon'].startswith('http'):
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster-blur" style="background-image: url(${data['channel_icon']});"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(${data['channel_icon']});"></div>
|
||||
% else:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['channel_icon']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover&refresh=true);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['channel_icon']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
||||
% endif
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['channel_icon'], data['rating_key'], 300, 300, 60, '282828', 3, fallback='cover', refresh=True)});"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(${page('pms_image_proxy', data['channel_icon'], data['rating_key'], 300, 300, fallback='cover', refresh=True)});"></div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="dashboard-activity-info-icon">
|
||||
@@ -139,7 +143,7 @@ DOCUMENTATION :: END
|
||||
<div id="platform-${sk}" class="dashboard-activity-info-platform${no_terminate} svg-icon platform-${data['platform_name']}" title="${data['platform']}"></div>
|
||||
% if _session['user_group'] == 'admin' and plexpy.CONFIG.PMS_PLEXPASS and data['session_id']:
|
||||
<div class="dashboard-activity-terminate-session" id="terminate-button-${sk}" data-key="${sk}" data-id="${data['session_id']}" data-toggle="tooltip" title="Terminate Stream">
|
||||
<i class="fa fa-times" style="padding-top: 8px;"></i>
|
||||
<i class="fa fa-times" style="padding-top: 10px;"></i>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
@@ -160,7 +164,7 @@ DOCUMENTATION :: END
|
||||
<div class="sub-value platform-right" id="stream_quality-${sk}">
|
||||
% if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown':
|
||||
<%
|
||||
br = helpers.cast_to_int(data['stream_bitrate']) or ''
|
||||
br = cast_to_int(data['stream_bitrate']) or ''
|
||||
if br:
|
||||
if br > 1000:
|
||||
br = '(' + str(round(br / 1000.0, 1)) + ' Mbps)'
|
||||
@@ -226,17 +230,24 @@ DOCUMENTATION :: END
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Video</div>
|
||||
<div class="sub-value" id="video_decision-${sk}">
|
||||
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
||||
% if data['media_type'] in ('movie', 'episode', 'clip') and data['stream_video_decision']:
|
||||
<%
|
||||
if data['video_dynamic_range'] == 'HDR':
|
||||
video_dynamic_range = ' ' + data['video_dynamic_range']
|
||||
stream_video_dynamic_range = ' ' + data['stream_video_dynamic_range']
|
||||
else:
|
||||
video_dynamic_range = stream_video_dynamic_range = ''
|
||||
%>
|
||||
% if data['stream_video_decision'] == 'transcode':
|
||||
<%
|
||||
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
||||
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
||||
%>
|
||||
Transcode (${data['video_codec'].upper()}${hw_d} ${data['video_full_resolution']} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${data['stream_video_full_resolution']})
|
||||
Transcode (${data['video_codec'].upper()}${hw_d} ${data['video_full_resolution']}${video_dynamic_range} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
|
||||
% elif data['stream_video_decision'] == 'copy':
|
||||
Direct Stream (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']})
|
||||
Direct Stream (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
|
||||
% else:
|
||||
Direct Play (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']})
|
||||
Direct Play (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
|
||||
% endif
|
||||
% elif data['media_type'] == 'photo':
|
||||
Direct Play (${data['width']}x${data['height']})
|
||||
@@ -248,12 +259,14 @@ DOCUMENTATION :: END
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Audio</div>
|
||||
<div class="sub-value" id="audio_decision-${sk}">
|
||||
% if data['stream_audio_decision'] == 'transcode':
|
||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% elif data['stream_audio_decision'] == 'copy':
|
||||
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% else:
|
||||
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% if data['stream_audio_decision']:
|
||||
% if data['stream_audio_decision'] == 'transcode':
|
||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% elif data['stream_audio_decision'] == 'copy':
|
||||
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% else:
|
||||
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
</li>
|
||||
@@ -292,7 +305,11 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
<span id="location-${sk}">${data['location'].upper()}</span>:
|
||||
% if data['ip_address'] != 'N/A':
|
||||
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
% if len(data['ip_address']) > 20:
|
||||
<span class="ip-container"><span class="ip-address" data-toggle="tooltip" title="${data['ip_address']}">${data['ip_address']}</span></span>
|
||||
% else:
|
||||
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
% endif
|
||||
% if data['relayed']:
|
||||
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
|
||||
% else:
|
||||
@@ -317,8 +334,10 @@ DOCUMENTATION :: END
|
||||
<div class="sub-value time-right">
|
||||
% if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown':
|
||||
<%
|
||||
bw = helpers.cast_to_int(data['bandwidth'])
|
||||
if bw > 1000:
|
||||
bw = cast_to_int(data['bandwidth'])
|
||||
if bw > 1000000:
|
||||
bw = str(round(bw / 1000000.0, 1)) + ' Gbps'
|
||||
elif bw > 1000:
|
||||
bw = str(round(bw / 1000.0, 1)) + ' Mbps'
|
||||
else:
|
||||
bw = str(bw) + ' kbps'
|
||||
@@ -337,8 +356,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% if data['media_type'] != 'photo':
|
||||
<div class="dashboard-activity-info-time">
|
||||
% if data['live'] == 1:
|
||||
<br />Live
|
||||
% if data['live']:
|
||||
<br /><span class="thumb-tooltip" data-toggle="popover" data-img="${data['channel_thumb']}" data-height="40" data-width="40">${data['channel_call_sign']} ${data['channel_identifier']}</span>
|
||||
% elif data['view_offset']:
|
||||
ETA:
|
||||
<span id="stream-eta-${sk}">
|
||||
@@ -367,8 +386,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="dashboard-activity-progress">
|
||||
<div class="dashboard-activity-progress-bar">
|
||||
% if data['live'] == 1:
|
||||
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-toggle="tooltip" title="Stream Progress Live">Live</div>
|
||||
% if data['live']:
|
||||
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-state="live" data-toggle="tooltip" title="Stream Progress Live">Live</div>
|
||||
% else:
|
||||
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
|
||||
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
|
||||
@@ -391,7 +410,16 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-title">
|
||||
% if data['channel_stream'] == 0:
|
||||
% if data['live']:
|
||||
% if data['media_type'] == 'movie':
|
||||
<a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a href="${href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% else:
|
||||
<span title="${data['title']}">${data['title']}</span>
|
||||
% endif
|
||||
% elif data['channel_stream'] == 0:
|
||||
% if data['media_type'] == 'movie':
|
||||
<a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
@@ -416,9 +444,9 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-subtitle-container">
|
||||
% if data['live'] == 1:
|
||||
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Plex Live TV">
|
||||
<i class="fa fa-fw fa-television"></i>
|
||||
% if data['live']:
|
||||
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Live TV">
|
||||
<i class="fa fa-fw fa-broadcast-tower"></i>
|
||||
</div>
|
||||
% elif data['channel_stream'] == 0:
|
||||
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
|
||||
@@ -440,8 +468,19 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% endif
|
||||
<div class="dashboard-activity-metadata-subtitle">
|
||||
% if data['live'] == 1:
|
||||
<span title="Plex Live TV" class="sub-heading">Plex Live TV</span>
|
||||
% if data['live']:
|
||||
% if data['media_type'] == 'movie':
|
||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||
% elif data['media_type'] == 'episode':
|
||||
% if data['media_index']:
|
||||
<a href="${href}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
||||
· <a href="${href}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
||||
% else:
|
||||
<a href="${href}" title="${data['originally_available_at']}" class="sub-heading">${data['originally_available_at']}</a>
|
||||
% endif
|
||||
% else:
|
||||
<span title="Live TV" class="sub-heading">Live TV</span>
|
||||
% endif
|
||||
% elif data['channel_stream'] == 0:
|
||||
% if data['media_type'] == 'movie':
|
||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||
|
@@ -40,13 +40,22 @@ DOCUMENTATION :: END
|
||||
<div class="modal-body" id="modal-text">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label for="profile_url">Library Picture URL</label>
|
||||
<label for="profile_url">Library Thumbnail URL</label>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="custom_thumb_url" name="custom_thumb_url" value="${data['library_thumb']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Change the library's picture in Tautulli. To reset to default, leave this field empty and save.</p>
|
||||
<p class="help-block">Change the library's thumbnail in Tautulli. To reset to default, leave this field empty and save.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile_url">Library Background Art URL</label>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="custom_art_url" name="custom_art_url" value="${data['library_art']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Change the library's background art in Tautulli. To reset to default, leave this field empty and save.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
@@ -80,6 +89,7 @@ DOCUMENTATION :: END
|
||||
// Save library options
|
||||
$("#save_library").on('click', function () {
|
||||
var custom_thumb = $("#custom_thumb_url").val();
|
||||
var custom_art = $("#custom_art_url").val();
|
||||
var keep_history = 0;
|
||||
if ($("#keep_history").is(":checked")) {
|
||||
keep_history = 1;
|
||||
@@ -90,6 +100,7 @@ DOCUMENTATION :: END
|
||||
data: {
|
||||
section_id: '${data["section_id"]}',
|
||||
custom_thumb: custom_thumb,
|
||||
custom_art: custom_art,
|
||||
keep_history: keep_history
|
||||
},
|
||||
cache: false,
|
||||
|
@@ -252,6 +252,7 @@
|
||||
case "TV": media_type = 'episode'; break;
|
||||
case "Movies": media_type = 'movie'; break;
|
||||
case "Music": media_type = 'track'; break;
|
||||
case "Live TV": media_type = 'live'; break;
|
||||
case "Direct Play": transcode_decision = 'direct play'; break;
|
||||
case "Direct Stream": transcode_decision = 'copy'; break;
|
||||
case "Transcode": transcode_decision = 'transcode'; break;
|
||||
@@ -304,6 +305,23 @@
|
||||
|
||||
setLocalStorage(chart_key, JSON.stringify(chart_visibility));
|
||||
}
|
||||
|
||||
function getGraphColors(data_series) {
|
||||
var colors = {
|
||||
'TV': '#E5A00D',
|
||||
'Movies': '#FFFFFF',
|
||||
'Music': '#F06464',
|
||||
'Live TV': '#19A0D7',
|
||||
'Direct Play': '#E5A00D',
|
||||
'Direct Stream': '#FFFFFF',
|
||||
'Transcode': '#F06464'
|
||||
};
|
||||
var series_colors = [];
|
||||
$.each(data_series, function(index, series) {
|
||||
series_colors.push(colors[series.name]);
|
||||
});
|
||||
return series_colors;
|
||||
}
|
||||
</script>
|
||||
<script src="${http_root}js/graphs/plays_by_day.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/plays_by_dayofweek.js${cache_param}"></script>
|
||||
@@ -390,6 +408,7 @@
|
||||
hc_plays_by_day_options.yAxis.min = 0;
|
||||
hc_plays_by_day_options.xAxis.categories = dateArray;
|
||||
hc_plays_by_day_options.series = getGraphVisibility(hc_plays_by_day_options.chart.renderTo, data.series);
|
||||
hc_plays_by_day_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_day = new Highcharts.Chart(hc_plays_by_day_options);
|
||||
}
|
||||
});
|
||||
@@ -403,6 +422,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_dayofweek_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_dayofweek_options.series = getGraphVisibility(hc_plays_by_dayofweek_options.chart.renderTo, data.series);
|
||||
hc_plays_by_dayofweek_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_dayofweek = new Highcharts.Chart(hc_plays_by_dayofweek_options);
|
||||
}
|
||||
});
|
||||
@@ -416,6 +436,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_hourofday_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_hourofday_options.series = getGraphVisibility(hc_plays_by_hourofday_options.chart.renderTo, data.series);
|
||||
hc_plays_by_hourofday_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_hourofday = new Highcharts.Chart(hc_plays_by_hourofday_options);
|
||||
}
|
||||
});
|
||||
@@ -429,6 +450,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_platform_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_platform_options.series = getGraphVisibility(hc_plays_by_platform_options.chart.renderTo, data.series);
|
||||
hc_plays_by_platform_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_platform = new Highcharts.Chart(hc_plays_by_platform_options);
|
||||
}
|
||||
});
|
||||
@@ -442,6 +464,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_user_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_user_options.series = getGraphVisibility(hc_plays_by_user_options.chart.renderTo, data.series);
|
||||
hc_plays_by_user_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
|
||||
}
|
||||
});
|
||||
@@ -478,6 +501,7 @@
|
||||
hc_plays_by_stream_type_options.yAxis.min = 0;
|
||||
hc_plays_by_stream_type_options.xAxis.categories = dateArray;
|
||||
hc_plays_by_stream_type_options.series = getGraphVisibility(hc_plays_by_stream_type_options.chart.renderTo, data.series);
|
||||
hc_plays_by_stream_type_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_stream_type = new Highcharts.Chart(hc_plays_by_stream_type_options);
|
||||
}
|
||||
});
|
||||
@@ -491,6 +515,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_source_resolution_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_source_resolution_options.series = getGraphVisibility(hc_plays_by_source_resolution_options.chart.renderTo, data.series);
|
||||
hc_plays_by_source_resolution_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_source_resolution = new Highcharts.Chart(hc_plays_by_source_resolution_options);
|
||||
}
|
||||
});
|
||||
@@ -504,6 +529,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_stream_resolution_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_stream_resolution_options.series = getGraphVisibility(hc_plays_by_stream_resolution_options.chart.renderTo, data.series);
|
||||
hc_plays_by_stream_resolution_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_stream_resolution = new Highcharts.Chart(hc_plays_by_stream_resolution_options);
|
||||
}
|
||||
});
|
||||
@@ -517,6 +543,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_platform_by_stream_type_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_platform_by_stream_type_options.series = getGraphVisibility(hc_plays_by_platform_by_stream_type_options.chart.renderTo, data.series);
|
||||
hc_plays_by_platform_by_stream_type_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_platform_by_stream_type = new Highcharts.Chart(hc_plays_by_platform_by_stream_type_options);
|
||||
}
|
||||
});
|
||||
@@ -530,6 +557,7 @@
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
hc_plays_by_user_by_stream_type_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_user_by_stream_type_options.series = getGraphVisibility(hc_plays_by_user_by_stream_type_options.chart.renderTo, data.series);
|
||||
hc_plays_by_user_by_stream_type_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_user_by_stream_type = new Highcharts.Chart(hc_plays_by_user_by_stream_type_options);
|
||||
}
|
||||
});
|
||||
@@ -553,6 +581,7 @@
|
||||
hc_plays_by_month_options.yAxis.min = 0;
|
||||
hc_plays_by_month_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_month_options.series = getGraphVisibility(hc_plays_by_month_options.chart.renderTo, data.series);
|
||||
hc_plays_by_month_options.colors = getGraphColors(data.series);
|
||||
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
|
||||
}
|
||||
});
|
||||
|
@@ -44,6 +44,9 @@
|
||||
<label class="btn btn-dark">
|
||||
<input type="radio" name="media_type-filter" id="history-track" value="track" autocomplete="off"> Music
|
||||
</label>
|
||||
<label class="btn btn-dark">
|
||||
<input type="radio" name="media_type-filter" id="history-live" value="live" autocomplete="off"> Live TV
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
|
||||
@@ -182,17 +185,15 @@
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<h4 class="modal-title" id="myModalLabel">
|
||||
<strong><span id="modal_header_ip_address">
|
||||
% if data.get('media_type'):
|
||||
<% h = {'episode': 'TV Show', 'track': 'Music'} %>
|
||||
<% h = {'episode': 'TV Show', 'track': 'Music', 'live': 'Live TV'} %>
|
||||
<i class="fa fa-history"></i> ${h.get(data['media_type'], data['media_type'].title())} History for <span id="date-header">${data['start_date']}</span>
|
||||
% elif data.get('transcode_decision'):
|
||||
<% h = {'copy': 'Direct Stream'} %>
|
||||
|
@@ -53,11 +53,11 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<%!
|
||||
from plexpy import helpers
|
||||
from plexpy.helpers import cast_to_int, page
|
||||
|
||||
# Human readable duration
|
||||
def hd(seconds):
|
||||
m, s = divmod(helpers.cast_to_int(seconds), 60)
|
||||
m, s = divmod(cast_to_int(seconds), 60)
|
||||
h, m = divmod(m, 60)
|
||||
return str(h).zfill(1) + ':' + str(m).zfill(2)
|
||||
%>
|
||||
@@ -72,11 +72,8 @@ DOCUMENTATION :: END
|
||||
<div class="dashboard-stats-instance" id="stats-instance-${stat_id}" data-stat_id="${stat_id}">
|
||||
<div class="dashboard-stats-container">
|
||||
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
||||
% if row0['art']:
|
||||
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(pms_image_proxy?img=${row0['art']}&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art);">
|
||||
% else:
|
||||
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(images/art.png);">
|
||||
% endif
|
||||
<% fallback = 'art-live' if row0['live'] else 'art' %>
|
||||
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(${page('pms_image_proxy', row0['art'], row0['rating_key'], 500, 280, 40, '282828', 3, fallback=fallback)});">
|
||||
% elif stat_id == 'top_platforms':
|
||||
<div id="stats-background-${stat_id}" class="dashboard-stats-background platform-${row0['platform_name']}-rgba no-image">
|
||||
% else:
|
||||
@@ -85,20 +82,28 @@ DOCUMENTATION :: END
|
||||
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
||||
<div class="dashboard-stats-poster-container hidden-xs">
|
||||
% if stat_id in ('top_music', 'popular_music'):
|
||||
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover);"></div>
|
||||
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster" style="background-image: url(${page('pms_image_proxy', row0['thumb'], row0['rating_key'], 300, 300, 60, '282828', 3, fallback='cover')});"></div>
|
||||
% endif
|
||||
<% height, type = ('300', 'cover') if stat_id in ('top_music', 'popular_music') else ('450', 'poster') %>
|
||||
<% href = 'info?rating_key={}'.format(row0['rating_key']) if row0['rating_key'] else '#' %>
|
||||
<%
|
||||
height, fallback = ('450', 'poster')
|
||||
if stat_id in ('top_music', 'popular_music'):
|
||||
height, fallback = ('300', 'cover')
|
||||
elif row0['live']:
|
||||
height, fallback = ('450', 'poster-live')
|
||||
|
||||
href = '#'
|
||||
if row0['rating_key']:
|
||||
if row0['live']:
|
||||
href = page('info', row0['rating_key'], row0['guid'], history=True, live=row0['live'])
|
||||
else:
|
||||
href = page('info', row0['rating_key'])
|
||||
%>
|
||||
<a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}">
|
||||
% if row0['thumb']:
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=${height}&fallback=${type});"></div>
|
||||
% else:
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(images/${type}.png);"></div>
|
||||
% endif
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${fallback.split('-')[0]}" style="background-image: url(${page('pms_image_proxy', row0['thumb'], row0['rating_key'], 300, height, fallback=fallback)});"></div>
|
||||
</a>
|
||||
</div>
|
||||
% elif stat_id == 'top_users':
|
||||
<% user_href = 'user?user_id={}'.format(row0['user_id']) if row0['user_id'] else '#' %>
|
||||
<% user_href = page('user', row0['user_id']) if row0['user_id'] else '#' %>
|
||||
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
||||
</a>
|
||||
@@ -126,19 +131,27 @@ DOCUMENTATION :: END
|
||||
<div class="dashboard-stats-info scoller-content">
|
||||
<ul class="list-unstyled dashboard-stats-info-list">
|
||||
% for row in top_stat['rows']:
|
||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
|
||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}"
|
||||
data-rating_key="${row.get('rating_key')}" data-guid="${row.get('guid')}" data-title="${row.get('title')}"
|
||||
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
||||
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
|
||||
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
|
||||
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}" data-live="${row.get('live')}">
|
||||
<div class="sub-list">${loop.index + 1}</div>
|
||||
<div class="sub-value">
|
||||
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
||||
<% href = 'info?rating_key={}'.format(row['rating_key']) if row['rating_key'] else '#' %>
|
||||
<%
|
||||
href = '#'
|
||||
if row['rating_key']:
|
||||
if row['live']:
|
||||
href = page('info', row['rating_key'], row['guid'], history=True, live=row['live'])
|
||||
else:
|
||||
href = page('info', row['rating_key'])
|
||||
%>
|
||||
<a href="${href}" title="${row['title']}">
|
||||
${row['title']}
|
||||
</a>
|
||||
% elif stat_id == 'top_users':
|
||||
<% user_href = 'user?user_id={}'.format(row['user_id']) if row['user_id'] else '#' %>
|
||||
<% user_href = page('user', row['user_id']) if row['user_id'] else '#' %>
|
||||
<a href="${user_href}" title="${row['friendly_name']}">
|
||||
${row['friendly_name']}
|
||||
</a>
|
||||
@@ -170,78 +183,6 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% endif
|
||||
% endfor
|
||||
<script>
|
||||
$('.dashboard-stats-instance .dashboard-stats-info-scroller').scrollbar()
|
||||
|
||||
function changeImages(elem) {
|
||||
var stat_id = $(elem).data('stat_id');
|
||||
var art = $(elem).data('art');
|
||||
var thumb = $(elem).data('thumb');
|
||||
var user_id = $(elem).data('user_id');
|
||||
var user_thumb = $(elem).data('user_thumb');
|
||||
var rating_key = $(elem).data('rating_key');
|
||||
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
|
||||
var href;
|
||||
|
||||
if (stat_id == 'most_concurrent') {
|
||||
return
|
||||
} else if (stat_id == 'top_users') {
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
|
||||
if (user_id) {
|
||||
href = 'user?user_id=' + user_id;
|
||||
} else {
|
||||
href = '#';
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
|
||||
} else if (stat_id == 'top_platforms') {
|
||||
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
}).addClass('platform-' + $(elem).data('platform'));
|
||||
$('#stats-background-' + stat_id).removeClass(function (index, className) {
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
}).addClass('platform-' + $(elem).data('platform') + '-rgba');
|
||||
} else {
|
||||
if (rating_key) {
|
||||
href = 'info?rating_key=' + rating_key;
|
||||
} else {
|
||||
href = '#';
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
|
||||
if (art) {
|
||||
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art)');
|
||||
} else {
|
||||
$('#stats-background-' + stat_id).css('background-image', 'url(images/art.png)');
|
||||
}
|
||||
if (thumb) {
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
|
||||
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&opacity=60&background=282828&blur=3&fallback=' + fallback + ')');
|
||||
} else {
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
|
||||
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(images/' + fallback + '.png)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('.dashboard-stats-info-item').mouseenter(function () {
|
||||
changeImages(this)
|
||||
if ($(this).data('stat_id') == 'last_watched') {
|
||||
var friendly_name = $(this).data('friendly_name');
|
||||
var last_watch = moment($(this).data('last_watch'), 'X').format(date_format);
|
||||
$('#last-watched-header-info').html(friendly_name);
|
||||
} else if ($(this).data('stat_id') == 'most_concurrent') {
|
||||
var started = moment($(this).data('started'), 'X').format(date_format + ' ' + time_format);
|
||||
$('#most-concurrent-header-info').html(started);
|
||||
}
|
||||
});
|
||||
$('.dashboard-stats-instance').mouseleave(function () {
|
||||
changeImages($(this).find('.dashboard-stats-info-item').first())
|
||||
if ($(this).data('stat_id') == 'last_watched') {
|
||||
$('#last-watched-header-info').text($(this).find('.dashboard-stats-info-item').first().data('friendly_name'));
|
||||
} else if ($(this).data('stat_id') == 'most_concurrent') {
|
||||
$('#most-concurrent-header-info').text('streams');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% else:
|
||||
<div class="text-muted">No stats to show for the selected period.</div><br>
|
||||
% endif
|
BIN
data/interfaces/default/images/art-live-full.png
Normal file
After Width: | Height: | Size: 786 KiB |
@@ -2,7 +2,7 @@
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.5"/>
|
||||
<square150x150logo src="mstile-150x150.png?v=2.0.5"/>
|
||||
<TileColor>#282a2d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
|
@@ -1,18 +1,23 @@
|
||||
{
|
||||
"name": "Tautulli",
|
||||
"name": "Tautulli: Monitor your Plex Media Server",
|
||||
"short_name": "Tautulli",
|
||||
"Description": "A Python based monitoring and tracking tool for Plex Media Server.",
|
||||
"start_url": "../../",
|
||||
"scope": "../../",
|
||||
"icons": [
|
||||
{
|
||||
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.5",
|
||||
"src": "android-chrome-192x192.png?v=2.0.5",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.5",
|
||||
"src": "android-chrome-256x256.png?v=2.0.5",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#282a2d",
|
||||
"background_color": "#282a2d",
|
||||
"display": "standalone"
|
||||
"display": "standalone",
|
||||
"orientation": "any"
|
||||
}
|
BIN
data/interfaces/default/images/libraries/live.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
9
data/interfaces/default/images/libraries/live.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>live</title>
|
||||
<path fill="#fff" d="M9.636 10.115c-0.829 0.544-1.243 0.816-2.072 1.361-2.331-3.547-2.331-6.195 0-9.749 0.829 0.546 1.244 0.819 2.072 1.361-1.68 2.557-1.68 4.464 0 7.027z"></path>
|
||||
<path fill="#fff" d="M4.374 11.662c-0.828 0.542-1.243 0.815-2.072 1.359-3.069-4.676-3.069-8.159 0-12.838 0.829 0.546 1.244 0.817 2.072 1.362-2.418 3.684-2.418 6.426 0 10.117z"></path>
|
||||
<path fill="#fff" d="M22.365 10.115c0.826 0.544 1.242 0.816 2.070 1.361 2.334-3.547 2.334-6.195 0-9.749-0.828 0.546-1.244 0.819-2.070 1.361 1.677 2.557 1.677 4.464 0 7.027z"></path>
|
||||
<path fill="#fff" d="M27.627 11.662c0.827 0.542 1.243 0.815 2.070 1.359 3.070-4.676 3.070-8.159 0-12.838-0.827 0.546-1.243 0.817-2.070 1.362 2.419 3.684 2.419 6.426 0 10.117z"></path>
|
||||
<path fill="#fff" d="M25.211 31.982l2.611-0.95-8.172-22.45c0.32-0.589 0.502-1.263 0.502-1.979 0-2.293-1.859-4.152-4.152-4.152s-4.151 1.858-4.151 4.152c0 0.672 0.16 1.305 0.443 1.868l-8.212 22.561 2.612 0.95 1.952-5.362h14.616l1.951 5.362zM17.396 10.513l3.945 10.834-7.903-7.9 1.080-2.966c0.46 0.176 0.96 0.272 1.481 0.272 0.49 0.001 0.961-0.084 1.397-0.24zM12.39 16.329l7.51 7.512h-10.245l2.735-7.512z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
data/interfaces/default/images/rating/imdb.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1000 560" xmlns="http://www.w3.org/2000/svg" stroke-miterlimit="1.414" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round"><path d="M0 89.996C0 62.384 22.378 40 49.997 40h900.006C977.616 40 1000 62.388 1000 89.996v380.008c0 27.612-22.378 49.996-49.997 49.996H49.997C22.384 520 0 497.612 0 470.004V89.996z" fill="#e1be00"/><path d="M769.68 134.76v94.64c6.03-6.976 12.753-12.181 20.17-15.61 7.419-3.428 18.552-5.157 27.24-5.157 10.01 0 18.685 1.552 26.04 4.667 7.362 3.109 12.967 7.471 16.829 13.08 3.857 5.614 6.172 11.11 6.962 16.485.781 5.377 1.176 16.843 1.176 34.41v81.63c0 17.448-1.176 30.434-3.528 38.981-2.357 8.543-7.881 15.958-16.567 22.23-8.691 6.267-19 9.405-30.952 9.405-8.567 0-19.648-1.857-27.07-5.581-7.424-3.724-14.21-9.314-20.362-16.767l-4.709 18.538h-68.04v-290.95h72.809m-631.58 290.95h75.58v-290.95h-75.58v290.95m199.38-290.95c2.881 17.615 5.9 38.29 9.06 62.01l10.829 73.915 17.505-135.92h98.73v290.95h-65.99l-.239-196.38-26.433 196.38h-47.15l-27.862-192.11-.238 192.11h-66.2v-290.95h97.99m218.36 0c36.581 0 57.629 1.681 70.52 5.03 12.895 3.347 22.705 8.847 29.419 16.504 6.719 7.657 10.915 16.181 12.595 25.567 1.677 9.39 2.752 27.843 2.752 55.36v102.18c0 26.08-1.461 43.519-3.918 52.31-2.462 8.8-6.748 15.676-12.862 20.638-6.124 4.962-13.676 8.433-22.672 10.404-9 1.977-22.551 2.962-40.657 2.962h-91.57v-290.95h56.39m239.33 220.35c0 14.08-.7 22.977-2.096 26.677-1.4 3.704-7.485 5.566-12.1 5.566-4.5 0-7.5-1.786-9.02-5.371-1.519-3.581-2.272-11.757-2.272-24.538v-76.891c0-13.257.667-21.519 2-24.809 1.333-3.277 4.248-4.924 8.743-4.924 4.609 0 10.796 1.871 12.376 5.633 1.576 3.762 2.367 11.795 2.367 24.09v74.57m-203.37-167.99c2.986 1.728 4.901 4.457 5.734 8.157.833 3.709 1.257 12.138 1.257 25.29v112.8c0 19.371-1.257 31.23-3.767 35.595-2.509 4.371-9.2 6.548-20.06 6.548v-190.99c8.234 0 13.852.866 16.838 2.6"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560" viewBox="0 0 560 560">
|
||||
<g fill="none" transform="translate(33 140)">
|
||||
<path fill="#FFF" d="M43.8020066,267.3152 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.3152 Z M266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 C26.3585902,221.21181 24.03,207.991848 22.1723803,189.066133 L266.684852,192.017143 Z M275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.325333 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 L275.03101,89.3083429 Z M302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 Z"/>
|
||||
<path fill="#00641E" d="M303.565869,264.667352 C306.720846,256.846476 317.903331,252.081752 325.93259,252.63901 C334.515108,253.234819 343.631626,262.264838 345.224872,271.145905 C345.520761,270.823467 345.830656,270.518552 346.145803,270.218895 C348.901593,267.583314 352.35421,265.834438 356.132479,265.352533 C355.554708,262.790552 355.416393,260.048076 355.812079,257.233752 C357.207482,247.3328 365.145698,239.907962 374.234203,239.981562 C380.099449,240.028876 385.245108,243.032457 388.597928,247.646476 C388.897318,247.271467 389.228223,246.928 389.550374,246.577524 C393.384669,226.586362 395.814807,203.999924 396.368066,180.030857 C398.267705,97.6111238 377.375174,30.2776381 349.70522,29.6397714 C322.033515,29.0001524 298.061292,95.2962286 296.161652,177.715962 C296.161652,177.715962 294.696216,207.778057 303.565869,264.667352"/>
|
||||
<path fill="#FFD700" d="M490.910577,354.797562 C492.545843,352.0656 493.45977,348.7904 493.396741,345.310171 C493.927239,334.065143 486.214879,323.871543 475.484105,325.000076 C475.794,323.713829 475.997095,322.376762 476.075882,320.997638 C476.718433,309.733333 468.957049,300.025143 458.739266,299.315429 C458.515161,299.30141 458.294557,299.2944 458.072203,299.28739 C459.134951,296.492343 459.661948,293.371352 459.47461,290.06461 C458.945862,280.692876 452.488839,272.796648 444.088407,271.242286 C441.054236,270.681524 438.111108,270.960152 435.416597,271.894171 C432.870905,265.617143 427.525652,260.961067 421.023108,259.977981 C420.508367,249.786133 413.153174,241.397486 403.68299,240.740343 C397.728452,240.326781 392.257141,243.064 388.597928,247.646476 C385.245108,243.032457 380.099449,240.030629 374.234203,239.983314 C365.145698,239.907962 357.207482,247.3328 355.812079,257.235505 C355.416393,260.048076 355.554708,262.790552 356.132479,265.354286 C352.35421,265.834438 348.901593,267.585067 346.145803,270.218895 C345.830656,270.518552 345.520761,270.823467 345.224872,271.145905 C343.631626,262.264838 334.515108,253.236571 325.93259,252.63901 C317.903331,252.081752 306.575528,256.963886 303.565869,264.667352 C304.885987,278.101105 313.275915,314.72061 343.621121,347.570743 L343.890748,347.590019 C346.816367,350.239619 350.636656,351.699352 354.709062,351.33661 C357.233744,351.110552 359.558833,350.206324 361.56177,348.811429 L362.050249,348.844724 C364.720249,350.700495 367.929502,351.671314 371.320839,351.369905 C372.616446,351.254248 373.852525,350.942324 375.027325,350.497219 C377.933685,356.51139 384.41522,360.398171 391.649607,359.760305 C397.25223,359.266133 402.014459,356.164419 404.81402,351.799238 L405.720944,351.862324 C408.517003,354.636343 412.233993,356.252038 416.24337,356.131124 C419.557672,361.149943 425.645272,364.237638 432.363167,363.647086 C434.893102,363.424533 437.254957,362.692038 439.361193,361.582781 C442.875089,365.90941 448.642289,368.481905 454.969751,367.924648 C461.22543,367.376152 466.509403,363.904686 469.384249,359.104914 C472.208321,361.374248 475.746728,362.592152 479.503987,362.257448 C483.149193,361.931505 486.38821,360.208914 488.821849,357.603124 L489.228039,357.631162 C489.781298,356.828571 490.25402,356.006705 490.693475,355.177829 C490.70398,355.162057 490.712734,355.144533 490.721489,355.128762 C490.781016,355.018362 490.854551,354.909714 490.910577,354.797562"/>
|
||||
<path fill="#04A53C" d="M281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 Z M302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.316952 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 Z M33.8766098,243.950705 C26.3585902,221.21181 24.03,207.9936 22.1723803,189.066133 L266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 Z M33.8766098,108.391771 L275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.327086 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 Z M378.597246,25.7126857 C363.342354,7.93478095 352.390977,-0.411809524 343.010085,0.672914286 C341.261016,0.895466667 76.1168852,37.8952381 76.1168852,37.8952381 C34.0429377,42.1780571 0.416695082,103.32739 0,176.172114 C0.416695082,249.015086 34.0429377,310.164419 76.1168852,314.44899 C76.1168852,314.44899 341.758249,351.583695 343.010085,351.669562 C345.228374,351.655543 347.418649,351.357638 349.57741,350.803886 C347.476426,350.178286 345.538269,349.083048 343.890748,347.590019 L343.621121,347.570743 C313.275915,314.722362 304.885987,278.101105 303.565869,264.667352 C303.56937,264.656838 303.576374,264.648076 303.579875,264.637562 C303.576374,264.648076 303.56937,264.656838 303.565869,264.667352 C294.696216,207.778057 296.161652,177.715962 296.161652,177.715962 C298.061292,95.2962286 322.033515,29.0001524 349.70522,29.638019 C377.375174,30.2776381 398.267705,97.6111238 396.368066,180.030857 C395.814807,203.999924 393.384669,226.586362 389.550374,246.577524 C393.489718,242.226362 398.773692,240.393371 403.68299,240.740343 C404.586413,240.805181 405.465325,240.957638 406.326728,241.155657 C423.131095,149.125867 405.514348,59.262019 378.597246,25.7126857 Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560"><g fill="none"><path fill="#FFF" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
data/interfaces/default/images/rating/tomato-ripe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560"><g fill="none"><path fill="#FA320A" d="M478.29 296.976c-3.99-63.966-36.52-111.823-85.468-138.579.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.265 6.78 108.735 108.63 170.89 211.193 164.49 102.556-6.395 193.466-80.572 186.683-189.307"/><path fill="#00912D" d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023"/></g></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
data/interfaces/default/images/rating/tomato-rotten.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560"><path fill="#0AC855" d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -334,13 +334,13 @@
|
||||
streams_header = streams_header.replace(/, $/, '') + ')';
|
||||
$('#currentActivityHeader-streams').text(streams_header);
|
||||
|
||||
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps'));
|
||||
var bandwidth_header = ((total_bw > 1000000) ? ((total_bw / 1000000).toFixed(1) + ' Gbps') : ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps')));
|
||||
var lan_wan_bandwidth_header = '';
|
||||
if (lan_bw) {
|
||||
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
||||
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000000) ? ((lan_bw / 1000000).toFixed(1) + ' Gbps') : ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps'))) + ', ';
|
||||
}
|
||||
if (wan_bw) {
|
||||
lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
||||
lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000000) ? ((wan_bw / 1000000).toFixed(1) + ' Gbps') : ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps'))) + ', ';
|
||||
}
|
||||
if (lan_wan_bandwidth_header) {
|
||||
bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')';
|
||||
@@ -356,8 +356,10 @@
|
||||
var instance = $('#activity-instance-' + key);
|
||||
|
||||
// Create a new instance if it doesn't exist or recreate the entire instance
|
||||
// if the rating key changed (for movies or episodes) with the same session key
|
||||
if (!(instance.length) || (s.media_type !== 'track' && s.rating_key !== instance.data('rating_key').toString())) {
|
||||
// if the rating key changed (for movies or episodes) of guid changed (for live tv) with the same session key
|
||||
if (!(instance.length) ||
|
||||
(s.media_type !== 'track' && s.rating_key !== instance.data('rating_key').toString()) ||
|
||||
(s.live === 1 && s.guid !== instance.data('guid'))) {
|
||||
create_instances.push(key);
|
||||
getActivityInstance(key);
|
||||
return;
|
||||
@@ -384,32 +386,32 @@
|
||||
if (s.media_type === 'track') {
|
||||
// Update if artist changed
|
||||
if (s.grandparent_rating_key !== instance.data('grandparent_rating_key').toString()) {
|
||||
$('#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(' + page('pms_image_proxy', s.art, s.rating_key, 500, 280, 40, '282828', 3, 'art', true) + ')');
|
||||
$('#metadata-grandparent_title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
||||
.attr('href', page('info', s.grandparent_rating_key))
|
||||
.attr('title', s.original_title || s.grandparent_title)
|
||||
.text(s.original_title || s.grandparent_title);
|
||||
}
|
||||
// Update cover if album changed
|
||||
if (s.parent_rating_key !== instance.data('parent_rating_key').toString()) {
|
||||
$('#poster-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
|
||||
$('#poster-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&opacity=60&background=282828&blur=3&fallback=poster&refresh=true)');
|
||||
$('#poster-' + key).css('background-image', 'url(' + page('pms_image_proxy', s.parent_thumb, s.parent_rating_key, 300, 300, null, null, null, 'poster', true) + ')');
|
||||
$('#poster-' + key + '-bg').css('background-image', 'url(' + page('pms_image_proxy', s.parent_thumb, s.parent_rating_key, 300, 300, 60, '282828', 3, 'poster', true) + ')');
|
||||
$('#poster-url-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.parent_rating_key)
|
||||
.attr('href', page('info', s.parent_rating_key))
|
||||
.attr('title', s.parent_title);
|
||||
$('#metadata-parent_title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.parent_rating_key)
|
||||
.attr('href', page('info', s.parent_rating_key))
|
||||
.attr('title', s.parent_title)
|
||||
.text(s.parent_title);
|
||||
}
|
||||
// Update cover if track changed
|
||||
if (s.rating_key !== instance.data('rating_key').toString()) {
|
||||
$('#metadata-grandparent_title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
||||
.attr('href', page('info', s.grandparent_rating_key))
|
||||
.attr('title', s.original_title || s.grandparent_title)
|
||||
.text(s.original_title || s.grandparent_title);
|
||||
$('#metadata-title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.rating_key)
|
||||
.attr('href', page('info', s.rating_key))
|
||||
.attr('title', s.title)
|
||||
.text(s.title);
|
||||
}
|
||||
@@ -437,6 +439,8 @@
|
||||
|
||||
var video_decision = '';
|
||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) {
|
||||
var v_bd = (s.video_dynamic_range === 'HDR') ? ' ' + s.video_dynamic_range : '';
|
||||
var sv_bd = (s.video_dynamic_range === 'HDR') ? ' ' + s.stream_video_dynamic_range : '';
|
||||
var v_res= '';
|
||||
switch (s.video_resolution.toLowerCase()) {
|
||||
case 'sd':
|
||||
@@ -462,11 +466,11 @@
|
||||
if (s.stream_video_decision === 'transcode') {
|
||||
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
||||
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
||||
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
||||
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + v_bd + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + sv_bd + ')';
|
||||
} else if (s.stream_video_decision === 'copy') {
|
||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')';
|
||||
} else {
|
||||
video_decision = 'Direct Play (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
||||
video_decision = 'Direct Play (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')';
|
||||
}
|
||||
} else if (s.media_type === 'photo') {
|
||||
video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')';
|
||||
@@ -522,7 +526,9 @@
|
||||
|
||||
if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') {
|
||||
var bw = parseInt(s.bandwidth) || 0;
|
||||
if (bw > 1000) {
|
||||
if (bw > 1000000) {
|
||||
bw = (bw / 1000000).toFixed(1) + ' Gbps';
|
||||
} else if (bw > 1000) {
|
||||
bw = (bw / 1000).toFixed(1) + ' Mbps';
|
||||
} else {
|
||||
bw = bw + ' kbps'
|
||||
@@ -541,10 +547,12 @@
|
||||
// Update the progress bars, percent - 3 because of 3px padding-right
|
||||
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
|
||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||
var progress_bar = $('#progress-bar-' + key);
|
||||
progress_bar.data('state', s.state);
|
||||
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);
|
||||
if (s.live !== 1) {
|
||||
var progress_bar = $('#progress-bar-' + key);
|
||||
progress_bar.data('state', s.state);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Add temporary class so we know which instances are still active
|
||||
@@ -557,6 +565,7 @@
|
||||
$(instance).removeClass('updated-temp');
|
||||
} else {
|
||||
$(instance).find('[data-toggle=tooltip]').tooltip('destroy');
|
||||
$(instance).find('[data-toggle=popover]').popover('destroy');
|
||||
$(instance).remove();
|
||||
}
|
||||
});
|
||||
@@ -591,6 +600,17 @@
|
||||
|
||||
$('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar();
|
||||
$('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
$('#activity-instance-' + session_key + ' [data-toggle=popover]').popover({
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
delay: 50,
|
||||
template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||
content: function () {
|
||||
return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />';
|
||||
}
|
||||
});
|
||||
$('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
|
||||
lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller');
|
||||
|
||||
@@ -706,6 +726,88 @@
|
||||
% endif
|
||||
</script>
|
||||
% endif
|
||||
% if 'watch_stats' in config['home_sections'] or 'library_stats' in config['home_sections']:
|
||||
<script>
|
||||
function statsCardCallback() {
|
||||
$('.dashboard-stats-instance .dashboard-stats-info-scroller').scrollbar();
|
||||
|
||||
function changeImages(elem) {
|
||||
var stat_id = $(elem).data('stat_id');
|
||||
var art = $(elem).data('art');
|
||||
var thumb = $(elem).data('thumb');
|
||||
var user_id = $(elem).data('user_id');
|
||||
var user_thumb = $(elem).data('user_thumb');
|
||||
var rating_key = $(elem).data('rating_key');
|
||||
var guid = $(elem).data('guid');
|
||||
var live = $(elem).data('live');
|
||||
var [height, fallback_poster, fallback_art] = [450, 'poster', 'art'];
|
||||
if ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) {
|
||||
[height, fallback_poster, fallback_art] = [300, 'cover', 'art'];
|
||||
} else if (live) {
|
||||
[height, fallback_poster, fallback_art] = [450, 'poster-live', 'art-live'];
|
||||
}
|
||||
var href = '#';
|
||||
|
||||
if (stat_id === 'most_concurrent') {
|
||||
return
|
||||
} else if (stat_id === 'top_users') {
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
|
||||
if (user_id) {
|
||||
href = page('user', user_id);
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
|
||||
} else if (stat_id === 'top_platforms') {
|
||||
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
}).addClass('platform-' + $(elem).data('platform'));
|
||||
$('#stats-background-' + stat_id).removeClass(function (index, className) {
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
}).addClass('platform-' + $(elem).data('platform') + '-rgba');
|
||||
} else {
|
||||
if (rating_key) {
|
||||
if (live) {
|
||||
href = page('info', rating_key, guid, true, live);
|
||||
} else {
|
||||
href = page('info', rating_key);
|
||||
}
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
|
||||
$('#stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art, rating_key, 500, 280, 40, '282828', 3, fallback_art) + ')');
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', thumb, rating_key, 300, height, null, null, null, fallback_poster) + ')');
|
||||
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(' + page('pms_image_proxy', thumb, rating_key, 300, height, 60, '282828', 3, fallback_poster) + ')');
|
||||
$('#library-stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art, rating_key, 500, 280, 40, '282828', 3, fallback_art) + ')');
|
||||
if (thumb.startsWith('http')) {
|
||||
$('#library-stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', thumb, rating_key, 300, 300, null, null, null, 'cover') + ')')
|
||||
.removeClass('svg-icon library-' + stat_id);
|
||||
} else {
|
||||
$('#library-stats-thumb-' + stat_id).css('background-image', '')
|
||||
.addClass('svg-icon library-' + stat_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('.dashboard-stats-info-item').mouseenter(function () {
|
||||
changeImages(this);
|
||||
if ($(this).data('stat_id') === 'last_watched') {
|
||||
var friendly_name = $(this).data('friendly_name');
|
||||
var last_watch = moment($(this).data('last_watch'), 'X').format(date_format);
|
||||
$('#last-watched-header-info').html(friendly_name);
|
||||
} else if ($(this).data('stat_id') === 'most_concurrent') {
|
||||
var started = moment($(this).data('started'), 'X').format(date_format + ' ' + time_format);
|
||||
$('#most-concurrent-header-info').html(started);
|
||||
}
|
||||
});
|
||||
$('.dashboard-stats-instance').mouseleave(function () {
|
||||
changeImages($(this).find('.dashboard-stats-info-item').first());
|
||||
if ($(this).data('stat_id') === 'last_watched') {
|
||||
$('#last-watched-header-info').text($(this).find('.dashboard-stats-info-item').first().data('friendly_name'));
|
||||
} else if ($(this).data('stat_id') === 'most_concurrent') {
|
||||
$('#most-concurrent-header-info').text('streams');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
% endif
|
||||
% if 'watch_stats' in config['home_sections']:
|
||||
<script>
|
||||
function getHomeStats(time_range, stats_type) {
|
||||
@@ -724,6 +826,7 @@
|
||||
$("#home-stats").html(xhr.responseText);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
lockScroll('#home-stats .dashboard-stats-info-scroller');
|
||||
statsCardCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -763,6 +866,7 @@
|
||||
data: { },
|
||||
complete: function (xhr, status) {
|
||||
$("#library-stats").html(xhr.responseText);
|
||||
statsCardCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -36,10 +36,12 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<%!
|
||||
from collections import defaultdict
|
||||
import re
|
||||
|
||||
from plexpy import notifiers
|
||||
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
|
||||
from plexpy.helpers import page, get_percent
|
||||
|
||||
# Get audio codec file
|
||||
def af(codec):
|
||||
@@ -48,13 +50,20 @@ DOCUMENTATION :: END
|
||||
return file_type
|
||||
return codec
|
||||
|
||||
# Get audio codec file
|
||||
# Get video codec file
|
||||
def vf(codec):
|
||||
for pattern, file_type in MEDIA_FLAGS_VIDEO.iteritems():
|
||||
if re.match(pattern, codec):
|
||||
return file_type
|
||||
return codec
|
||||
|
||||
# Get video resolution file
|
||||
def vr(resolution):
|
||||
if resolution in ('1080i', '576i', '480i'):
|
||||
return resolution
|
||||
else:
|
||||
return resolution.lower().rstrip('ip')
|
||||
|
||||
def br(text):
|
||||
return text.replace('\n', '<br /><br />')
|
||||
%>
|
||||
@@ -68,11 +77,15 @@ DOCUMENTATION :: END
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
% if data:
|
||||
<% media_info = data['media_info'][0] if data['media_info'] else {} %>
|
||||
% if metadata:
|
||||
<%
|
||||
data = defaultdict(lambda: None, **metadata)
|
||||
media_info = defaultdict(lambda: None, **(data['media_info'][0] if data['media_info'] else {}))
|
||||
%>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
|
||||
<% fallback = 'art-live-full' if data['live'] else None %>
|
||||
<div class="art-face" style="background-image:url(${page('pms_image_proxy', data['art'], data['rating_key'], 1920, 1080, fallback=fallback)})"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -81,44 +94,60 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-12">
|
||||
<div class="summary-navbar-list">
|
||||
<ul class="list-unstyled breadcrumb">
|
||||
% if data['media_type'] in ('movie', 'collection'):
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
% if data['live']:
|
||||
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
% if data['media_type'] == 'movie':
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<li class="hidden-xs hidden-sm">${data['grandparent_title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
% if data['media_index']:
|
||||
<li>Season ${data['parent_media_index']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
|
||||
% else:
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% endif
|
||||
% endif
|
||||
% elif data['media_type'] in ('movie', 'collection'):
|
||||
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'show':
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% 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="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Season ${data['media_index']}</li>
|
||||
% 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="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<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="${page('info', data['grandparent_rating_key'])}">${data['grandparent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">Season ${data['parent_media_index']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
|
||||
% elif data['media_type'] == 'artist':
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% 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="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == '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="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<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="${page('info', data['grandparent_rating_key'])}">${data['grandparent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
|
||||
% endif
|
||||
@@ -131,11 +160,18 @@ DOCUMENTATION :: END
|
||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||
% if data['media_type'] == 'track':
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
|
||||
% else:
|
||||
% elif not data['live']:
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
|
||||
% endif
|
||||
% if data['live']:
|
||||
<div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live')});">
|
||||
<div class="summary-poster-face-overlay">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
% if data['media_type'] == 'episode':
|
||||
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
|
||||
<div class="summary-poster-face-episode" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 280, fallback='art')});">
|
||||
<div class="summary-poster-face-overlay">
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -144,7 +180,7 @@ DOCUMENTATION :: END
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=cover);">
|
||||
<div class="summary-poster-face-track" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 500, fallback='cover')});">
|
||||
<div class="summary-poster-face-overlay">
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -153,7 +189,7 @@ DOCUMENTATION :: END
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
% else:
|
||||
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster')});">
|
||||
<div class="summary-poster-face-overlay">
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -162,24 +198,37 @@ DOCUMENTATION :: END
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
% if not data['live']:
|
||||
</a>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-title">
|
||||
% if data['media_type'] in ('movie', 'show', 'artist', 'collection'):
|
||||
% if data['live']:
|
||||
% if data['media_type'] == 'movie':
|
||||
<h1> </h1><h1>${data['title']}</h1>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<h1>${data['grandparent_title']}</h1>
|
||||
<h2>${data['title']}</h2>
|
||||
% if data['media_index']:
|
||||
<h3 class="hidden-xs">S${data['parent_media_index']} · E${data['media_index']}</h3>
|
||||
% endif
|
||||
% endif
|
||||
% elif data['media_type'] in ('movie', 'show', 'artist', 'collection'):
|
||||
<h1> </h1><h1>${data['title']}</h1>
|
||||
% elif data['media_type'] == 'season':
|
||||
<h1> </h1><h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||
<h1> </h1><h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
|
||||
<h3 class="hidden-xs">S${data['media_index']}</h3>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
|
||||
<h1><a href="${page('info', data['grandparent_rating_key'])}">${data['grandparent_title']}</a></h1>
|
||||
<h2>${data['title']}</h2>
|
||||
<h3 class="hidden-xs">S${data['parent_media_index']} · E${data['media_index']}</h3>
|
||||
% elif data['media_type'] == 'album':
|
||||
<h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||
<h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
|
||||
<h2>${data['title']}</h2>
|
||||
% elif data['media_type'] == 'track':
|
||||
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['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>
|
||||
<h1><a href="${page('info', data['grandparent_rating_key'])}">${data['original_title'] or data['grandparent_title']}</a></h1>
|
||||
<h2><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a> - ${data['title']}</h2>
|
||||
<h3 class="hidden-xs">T${data['media_index']}</h3>
|
||||
% endif
|
||||
</div>
|
||||
@@ -187,7 +236,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="summary-content-wrapper">
|
||||
<div class="col-md-9">
|
||||
% if data['media_type'] == 'movie':
|
||||
% if data['media_type'] == 'movie' or data['live']:
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
|
||||
% elif data['media_type'] in ('show', 'season', 'collection'):
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
|
||||
@@ -206,7 +255,7 @@ DOCUMENTATION :: END
|
||||
<img class="summary-content-media-flag" title="${media_info['video_codec']}" src="${http_root}images/media_flags/video_codec/${media_info['video_codec'] | vf}.png" />
|
||||
% endif
|
||||
% if data['media_type'] != 'track' and media_info['video_resolution']:
|
||||
<img class="summary-content-media-flag" title="${media_info['video_resolution']}" src="${http_root}images/media_flags/video_resolution/${media_info['video_resolution']}.png" />
|
||||
<img class="summary-content-media-flag" title="${media_info['video_resolution']}" src="${http_root}images/media_flags/video_resolution/${media_info['video_full_resolution'] | vr}.png" />
|
||||
% endif
|
||||
% if media_info['audio_codec']:
|
||||
<img class="summary-content-media-flag" title="${media_info['audio_codec']}" src="${http_root}images/media_flags/audio_codec/${media_info['audio_codec'] | af}.png" />
|
||||
@@ -220,16 +269,28 @@ DOCUMENTATION :: END
|
||||
<div class="summary-content">
|
||||
<div class="summary-content-details-wrapper">
|
||||
% if data['rating']:
|
||||
<div class="star-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
% for i in range(0,5):
|
||||
% if round(float(data['rating']) / 2) > i:
|
||||
<i class="star-icon fa fa-star"></i>
|
||||
% else:
|
||||
<i class="star-icon-o fa fa-star-o"></i>
|
||||
% endif
|
||||
% endfor
|
||||
% if data['rating_image']:
|
||||
% if data['rating_image'].startswith('imdb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% if data['audience_rating_image'].startswith('rottentomatoes://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['audience_rating']}">
|
||||
<span class="rating-image rating-rottentomatos-${data['audience_rating_image'].rsplit('.')[-1]}"><strong>${get_percent(data['audience_rating'], 10)}%</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% if data['rating_image'].startswith('rottentomatoes://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<span class="rating-image rating-rottentomatos-${data['rating_image'].rsplit('.')[-1]}"><strong>${get_percent(data['rating'], 10)}%</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% else:
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<i class="star-icon fa fa-star"></i> <strong>${get_percent(data['rating'], 10)}%</strong>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="summary-content-details-tag">
|
||||
% if data['directors']:
|
||||
Directed by <strong> ${data['directors'][0]}</strong>
|
||||
@@ -251,6 +312,8 @@ DOCUMENTATION :: END
|
||||
Released <strong> ${data['year']}</strong>
|
||||
% elif data['media_type'] == 'collection':
|
||||
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
|
||||
% elif data['year']:
|
||||
Year <strong> ${data['year']}</strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-details-tag">
|
||||
@@ -263,6 +326,11 @@ DOCUMENTATION :: END
|
||||
Rated <strong> ${data['content_rating']} </strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-details-tag" id="channel-icon">
|
||||
% if media_info['channel_identifier']:
|
||||
Channel <strong> <span class="thumb-tooltip" data-toggle="popover" data-img="${media_info['channel_thumb']}" data-height="40" data-width="40">${media_info['channel_call_sign']} ${media_info['channel_identifier']}</span> </strong>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% if data['tagline']:
|
||||
<div class="summary-content-summary">
|
||||
@@ -415,7 +483,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% endif
|
||||
% if data.get('poster_url'):
|
||||
<div class="btn-group">
|
||||
<div class="btn-group" id="hosted-poster">
|
||||
% if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||
<span class="hosted-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="80" data-width="80" style="display: inline-flex;">
|
||||
% else:
|
||||
@@ -429,6 +497,7 @@ DOCUMENTATION :: END
|
||||
</span>
|
||||
</div>
|
||||
% endif
|
||||
% if not data['live']:
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark" data-toggle="modal" aria-pressed="false" autocomplete="off" id="send-recently-added-notification"
|
||||
data-id="${data['rating_key']}">
|
||||
@@ -436,6 +505,7 @@ DOCUMENTATION :: END
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
|
||||
</div>
|
||||
@@ -474,6 +544,10 @@ DOCUMENTATION :: END
|
||||
</%def>
|
||||
|
||||
<%def name="modalIncludes()">
|
||||
% if metadata:
|
||||
<%
|
||||
data = defaultdict(None, **metadata)
|
||||
%>
|
||||
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||
</div>
|
||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||
@@ -549,6 +623,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
@@ -558,9 +633,28 @@ DOCUMENTATION :: END
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
|
||||
% if data:
|
||||
% if metadata:
|
||||
<%
|
||||
data = defaultdict(None, **metadata)
|
||||
%>
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
% if data['media_type'] in ('show', 'artist'):
|
||||
% if data['live']:
|
||||
<script>
|
||||
function get_history() {
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
guid: "${data['guid']}",
|
||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
% elif data['media_type'] in ('show', 'artist'):
|
||||
<script>
|
||||
function get_history() {
|
||||
history_table_options.ajax = {
|
||||
@@ -627,17 +721,15 @@ DOCUMENTATION :: END
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function (row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
@@ -724,10 +816,22 @@ DOCUMENTATION :: END
|
||||
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
||||
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
$('#channel-icon').popover({
|
||||
selector: '[data-toggle=popover]',
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||
content: function () {
|
||||
return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% if data.get('poster_url'):
|
||||
<script>
|
||||
$('.hosted-poster-tooltip').popover({
|
||||
$('#hosted-poster').popover({
|
||||
selector: '[data-toggle=popover]',
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
@@ -754,10 +858,10 @@ DOCUMENTATION :: END
|
||||
% if data.get('tvmaze_id') or data.get('themoviedb_id') or data.get('musicbrainz_id'):
|
||||
<script>
|
||||
$('#delete-lookup-info').on('click', function () {
|
||||
var msg = 'Are you sure you want to delete the 3rd party API lookup for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
|
||||
'The info will be looked up again the next time a notification is sent.';
|
||||
var msg = 'Are you sure you want to delete all the metadata lookup info for <strong>' + $(this).data('title') + '</strong>?' +
|
||||
'<br /><br />Tautulli will lookup the metadata info again the next time a notification is sent.';
|
||||
var url = 'delete_lookup_info';
|
||||
var data = { rating_key: $(this).data('id'), title: $(this).data('title') };
|
||||
var data = { rating_key: $(this).data('id') };
|
||||
var callback = function () {
|
||||
$('#delete-lookup-info').closest('.btn-group').remove();
|
||||
};
|
||||
|
@@ -27,6 +27,9 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
%>
|
||||
% if data['children_count'] > 0:
|
||||
<div class="item-children-wrapper">
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
@@ -38,9 +41,9 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
% endif
|
||||
% if data['children_type'] == 'movie':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -48,14 +51,14 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">${child['year']}</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'show':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -63,16 +66,16 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'season':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="Season ${child['media_index']}">
|
||||
<a href="${page('info', child['rating_key'])}" title="Season ${child['media_index']}">
|
||||
<div class="item-children-poster">
|
||||
% if child['thumb']:
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});">
|
||||
% else:
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 450, fallback='poster')});">
|
||||
% endif
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
@@ -86,9 +89,9 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</a>
|
||||
% elif data['children_type'] == 'episode':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}">
|
||||
<a href="${page('info', child['rating_key'])}" title="Episode ${child['media_index']}">
|
||||
<div class="item-children-poster">
|
||||
<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(${page('pms_image_proxy', child['thumb'], child['rating_key'], 500, 280, fallback='art')});">
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
Episode ${child['media_index'] or child['originally_available_at']}
|
||||
@@ -102,13 +105,13 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper episode-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'album':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -116,14 +119,14 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'track':
|
||||
% if loop.index % 2 == 0:
|
||||
<div class="item-children-list-item-even">
|
||||
<span class="item-children-list-item-index"> ${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 class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
% if child['original_title']:
|
||||
<span class="text-muted"> - ${child['original_title']}</span>
|
||||
% endif
|
||||
@@ -135,7 +138,7 @@ DOCUMENTATION :: END
|
||||
% else:
|
||||
<div class="item-children-list-item-odd">
|
||||
<span class="item-children-list-item-index"> ${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
% if child['original_title']:
|
||||
<span class="text-muted"> - ${child['original_title']}</span>
|
||||
% endif
|
||||
|
@@ -29,6 +29,7 @@ DOCUMENTATION :: END
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.common import MEDIA_TYPE_HEADERS
|
||||
from plexpy.helpers import page
|
||||
types = ('movie', 'show', 'artist', 'album')
|
||||
%>
|
||||
% for media_type in types:
|
||||
@@ -45,12 +46,12 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list'][media_type]:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
% if media_type in ('artist', 'album'):
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
|
||||
% else:
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% endif
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
@@ -60,22 +61,22 @@ DOCUMENTATION :: END
|
||||
% if media_type == 'artist':
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% elif media_type == 'album':
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['parent_rating_key']}" title="${child['parent_title']}">${child['parent_title']}</a>
|
||||
<a href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>
|
||||
</h3>
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% else:
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">${child['year']}</h3>
|
||||
</div>
|
||||
|
@@ -53,6 +53,9 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
%>
|
||||
% if data['results_count'] > 0:
|
||||
% if 'collection' in data['results_list'] and data['results_list']['collection']:
|
||||
<div class="item-children-wrapper">
|
||||
@@ -62,9 +65,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['collection']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -87,9 +90,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['movie']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -112,9 +115,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['show']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -137,9 +140,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['season']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -162,9 +165,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['episode']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<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>
|
||||
<div class="item-children-poster-face episode-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 500, 280, fallback='art')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -188,9 +191,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['artist']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -212,9 +215,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['album']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -237,9 +240,9 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['track']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')});">
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
Track ${child['media_index']}
|
||||
|
@@ -40,7 +40,6 @@ var hc_plays_by_day_options = {
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_dayofweek_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_hourofday_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_month_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
labels: {
|
||||
style: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_platform_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_platform_by_stream_type_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_source_resolution_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_stream_resolution_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -40,7 +40,6 @@ var hc_plays_by_stream_type_options = {
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_user_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -23,7 +23,6 @@ var hc_plays_by_user_by_stream_type_options = {
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
|
@@ -258,33 +258,31 @@ $.cachedScript = function (url) {
|
||||
function isPrivateIP(ip_address) {
|
||||
var defer = $.Deferred();
|
||||
|
||||
$.cachedScript('js/ipaddr.min.js').done(function () {
|
||||
if (ipaddr.isValid(ip_address)) {
|
||||
var addr = ipaddr.process(ip_address);
|
||||
if (ipaddr.isValid(ip_address)) {
|
||||
var addr = ipaddr.process(ip_address);
|
||||
|
||||
var rangeList = [];
|
||||
if (addr.kind() === 'ipv4') {
|
||||
rangeList = [
|
||||
ipaddr.parseCIDR('127.0.0.0/8'),
|
||||
ipaddr.parseCIDR('10.0.0.0/8'),
|
||||
ipaddr.parseCIDR('172.16.0.0/12'),
|
||||
ipaddr.parseCIDR('192.168.0.0/16')
|
||||
];
|
||||
} else {
|
||||
rangeList = [
|
||||
ipaddr.parseCIDR('fd00::/8')
|
||||
];
|
||||
}
|
||||
|
||||
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
|
||||
defer.resolve();
|
||||
} else {
|
||||
defer.reject();
|
||||
}
|
||||
var rangeList = [];
|
||||
if (addr.kind() === 'ipv4') {
|
||||
rangeList = [
|
||||
ipaddr.parseCIDR('127.0.0.0/8'),
|
||||
ipaddr.parseCIDR('10.0.0.0/8'),
|
||||
ipaddr.parseCIDR('172.16.0.0/12'),
|
||||
ipaddr.parseCIDR('192.168.0.0/16')
|
||||
];
|
||||
} else {
|
||||
defer.resolve('n/a');
|
||||
rangeList = [
|
||||
ipaddr.parseCIDR('fd00::/8')
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
|
||||
defer.resolve();
|
||||
} else {
|
||||
defer.reject();
|
||||
}
|
||||
} else {
|
||||
defer.resolve('n/a');
|
||||
}
|
||||
|
||||
return defer.promise();
|
||||
}
|
||||
@@ -463,8 +461,9 @@ $('*').on('click', '.refresh_pms_image', function (e) {
|
||||
});
|
||||
|
||||
// Taken from http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable#answer-14919494
|
||||
function humanFileSize(bytes, si) {
|
||||
var thresh = si ? 1000 : 1024;
|
||||
function humanFileSize(bytes, si = true) {
|
||||
//var thresh = si ? 1000 : 1024;
|
||||
var thresh = 1024; // Always divide by 2^10 but display SI units
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
@@ -717,3 +716,69 @@ function encodeData(data) {
|
||||
return [key, data[key]].map(encodeURIComponent).join("=");
|
||||
}).join("&");
|
||||
}
|
||||
|
||||
function page(endpoint, ...args) {
|
||||
let endpoints = {
|
||||
'pms_image_proxy': pms_image_proxy,
|
||||
'info': info_page,
|
||||
'library': library_page,
|
||||
'user': user_page
|
||||
};
|
||||
|
||||
var params = {};
|
||||
|
||||
if (endpoint in endpoints) {
|
||||
params = endpoints[endpoint](...args);
|
||||
}
|
||||
|
||||
return endpoint + '?' + $.param(params).replace(/'/g, '%27');
|
||||
}
|
||||
|
||||
function pms_image_proxy(img, rating_key, width, height, opacity, background, blur, fallback, refresh, clip, img_format) {
|
||||
var params = {};
|
||||
|
||||
if (img != null) { params.img = img; }
|
||||
if (rating_key != null) { params.rating_key = rating_key; }
|
||||
if (width != null) { params.width = width; }
|
||||
if (height != null) { params.height = height; }
|
||||
if (opacity != null) { params.opacity = opacity; }
|
||||
if (background != null) { params.background = background; }
|
||||
if (blur != null) { params.blur = blur; }
|
||||
if (fallback != null) { params.fallback = fallback; }
|
||||
if (refresh != null) { params.refresh = true; }
|
||||
if (clip != null) { params.clip = true; }
|
||||
if (img_format != null) { params.img_format = img_format; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function info_page(rating_key, guid, history, live) {
|
||||
var params = {};
|
||||
|
||||
if (live && history) {
|
||||
params.guid = guid;
|
||||
} else {
|
||||
params.rating_key = rating_key;
|
||||
}
|
||||
|
||||
if (history) { params.source = 'history'; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function library_page(section_id) {
|
||||
var params = {};
|
||||
|
||||
if (section_id != null) { params.section_id = section_id; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function user_page(user_id, user) {
|
||||
var params = {};
|
||||
|
||||
if (user_id != null) { params.user_id = user_id; }
|
||||
if (user != null) { params.user = user; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
@@ -36,10 +36,10 @@ history_table_options = {
|
||||
"targets": [0],
|
||||
"data": null,
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (rowData['id'] === null) {
|
||||
if (rowData['row_id'] === null) {
|
||||
$(td).html('');
|
||||
} else {
|
||||
$(td).html('<button class="btn btn-xs btn-warning" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
|
||||
$(td).html('<button class="btn btn-xs btn-warning" data-id="' + rowData['row_id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
|
||||
}
|
||||
},
|
||||
"width": "5%",
|
||||
@@ -81,9 +81,9 @@ history_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['user_id']) {
|
||||
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
@@ -115,7 +115,7 @@ history_table_options = {
|
||||
"data": "platform",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
$(td).html(capitalizeFirstLetter(cellData));
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"width": "10%",
|
||||
@@ -156,29 +156,37 @@ history_table_options = {
|
||||
"data": "full_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var icon = '';
|
||||
var icon_title = '';
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
var source = (rowData['state'] === null) ? 'source=history&' : '';
|
||||
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
|
||||
var history = (rowData['state'] === null);
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
|
||||
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'clip') {
|
||||
$(td).html(cellData);
|
||||
} else {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -309,19 +317,19 @@ history_table_options = {
|
||||
"rowCallback": function (row, rowData, rowIndex) {
|
||||
if (rowData['group_count'] == 1) {
|
||||
// if no grouped rows simply toggle the delete button
|
||||
if ($.inArray(rowData['id'], history_to_delete) !== -1) {
|
||||
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
if ($.inArray(rowData['row_id'], history_to_delete) !== -1) {
|
||||
$(row).find('button[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
}
|
||||
} else if (rowData['id'] !== null) {
|
||||
} else if (rowData['row_id'] !== null) {
|
||||
// if grouped rows
|
||||
// toggle the parent button to danger
|
||||
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(row).find('button[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
// check if any child rows are not selected
|
||||
var group_ids = rowData['group_ids'].split(',').map(Number);
|
||||
group_ids.forEach(function (id) {
|
||||
var index = $.inArray(id, history_to_delete);
|
||||
if (index == -1) {
|
||||
$(row).find('button[data-id="' + rowData['id'] + '"]').addClass('btn-warning').removeClass('btn-danger');
|
||||
$(row).find('button[data-id="' + rowData['row_id'] + '"]').addClass('btn-warning').removeClass('btn-danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -345,7 +353,7 @@ $('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||
var rowData = row.data();
|
||||
|
||||
$.get('get_stream_data', {
|
||||
row_id: rowData['id'],
|
||||
row_id: rowData['row_id'],
|
||||
session_key: rowData['session_key'],
|
||||
user: rowData['friendly_name']
|
||||
}).then(function (jqXHR) {
|
||||
@@ -374,9 +382,9 @@ $('.history_table').on('click', '> tbody > tr > td.delete-control > button', fun
|
||||
|
||||
if (rowData['group_count'] == 1) {
|
||||
// if no grouped rows simply add or remove row from history_to_delete
|
||||
var index = $.inArray(rowData['id'], history_to_delete);
|
||||
var index = $.inArray(rowData['row_id'], history_to_delete);
|
||||
if (index === -1) {
|
||||
history_to_delete.push(rowData['id']);
|
||||
history_to_delete.push(rowData['row_id']);
|
||||
} else {
|
||||
history_to_delete.splice(index, 1);
|
||||
}
|
||||
@@ -541,7 +549,7 @@ function createChildTable(row, rowData) {
|
||||
var childRowData = childRow.data();
|
||||
|
||||
$.get('get_stream_data', {
|
||||
row_id: childRowData['id'],
|
||||
row_id: childRowData['row_id'],
|
||||
user: childRowData['friendly_name']
|
||||
}).then(function (jqXHR) {
|
||||
$("#info-modal").html(jqXHR);
|
||||
@@ -568,9 +576,9 @@ function createChildTable(row, rowData) {
|
||||
var childRowData = childRow.data();
|
||||
|
||||
// add or remove row from history_to_delete
|
||||
var index = $.inArray(childRowData['id'], history_to_delete);
|
||||
var index = $.inArray(childRowData['row_id'], history_to_delete);
|
||||
if (index === -1) {
|
||||
history_to_delete.push(childRowData['id']);
|
||||
history_to_delete.push(childRowData['row_id']);
|
||||
} else {
|
||||
history_to_delete.splice(index, 1);
|
||||
}
|
||||
|
@@ -63,9 +63,9 @@ history_table_modal_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['user_id']) {
|
||||
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
@@ -98,26 +98,34 @@ history_table_modal_options = {
|
||||
"data":"full_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var icon = '';
|
||||
var icon_title = '';
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
|
||||
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -161,7 +169,7 @@ $('.history_table').on('click', 'td.modal-control', function () {
|
||||
function showStreamDetails() {
|
||||
$.ajax({
|
||||
url: 'get_stream_data',
|
||||
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||
data: { row_id: rowData['row_id'], user: rowData['friendly_name'] },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
|
@@ -27,8 +27,8 @@ libraries_list_table_options = {
|
||||
"data": null,
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
$(td).html('<div class="edit-library-toggles">' +
|
||||
'<button class="btn btn-xs btn-warning delete-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||
'<button class="btn btn-xs btn-warning purge-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||
'<button class="btn btn-xs btn-warning delete-library" data-id="' + rowData['row_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||
'<button class="btn btn-xs btn-warning purge-library" data-id="' + rowData['row_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||
'<input type="checkbox" id="keep_history-' + rowData['section_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['section_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label> ' +
|
||||
'</div>');
|
||||
},
|
||||
@@ -41,14 +41,16 @@ libraries_list_table_options = {
|
||||
"targets": [1],
|
||||
"data": "library_thumb",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
var inactive = '';
|
||||
if (!rowData['is_active']) { inactive = '<span class="inactive-library-tooltip" data-toggle="tooltip" title="Library not on Plex server"><i class="fa fa-exclamation-triangle"></i></span>'; }
|
||||
if (cellData !== null && cellData !== '') {
|
||||
if (rowData['library_thumb'].substring(0, 4) == "http") {
|
||||
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(' + rowData['library_thumb'] + ');"></div></a>');
|
||||
$(td).html('<a href="' + page('library', rowData['section_id']) + '"><div class="libraries-poster-face" style="background-image: url(' + rowData['library_thumb'] + ');">' + inactive + '</div></a>');
|
||||
} else {
|
||||
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face svg-icon library-' + rowData['section_type'] + '"></div></a>');
|
||||
$(td).html('<a href="' + page('library', rowData['section_id']) + '"><div class="libraries-poster-face svg-icon library-' + rowData['section_type'] + '">' + inactive + '</div></a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(../../images/cover.png);"></div></a>');
|
||||
$(td).html('<a href="' + page('library', rowData['section_id']) + '"><div class="libraries-poster-face" style="background-image: url(../../images/cover.png);">' + inactive + '</div></a>');
|
||||
}
|
||||
},
|
||||
"orderable": false,
|
||||
@@ -61,8 +63,8 @@ libraries_list_table_options = {
|
||||
"data": "section_name",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html('<div data-id="' + rowData['section_id'] + '">' +
|
||||
'<a href="library?section_id=' + rowData['section_id'] + '">' + cellData + '</a>' +
|
||||
$(td).html('<div data-id="' + rowData['row_id'] + '">' +
|
||||
'<a href="' + page('library', rowData['section_id']) + '">' + cellData + '</a>' +
|
||||
'</div>');
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
@@ -137,45 +139,34 @@ libraries_list_table_options = {
|
||||
"data":"last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
var icon = '';
|
||||
var icon_title = '';
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
if (rowData['rating_key']) {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="images/poster.png" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
}
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
if (rowData['rating_key']) {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="images/poster.png" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
}
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
|
||||
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
if (rowData['rating_key']) {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="images/cover.png" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
}
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
if (rowData['rating_key']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
@@ -243,11 +234,11 @@ libraries_list_table_options = {
|
||||
showMsg(msg, false, false, 0)
|
||||
},
|
||||
"rowCallback": function (row, rowData) {
|
||||
if ($.inArray(rowData['section_id'], libraries_to_delete) !== -1) {
|
||||
$(row).find('button.delete-library[data-id="' + rowData['section_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
if ($.inArray(rowData['row_id'], libraries_to_delete) !== -1) {
|
||||
$(row).find('button.delete-library[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
}
|
||||
if ($.inArray(rowData['section_id'], libraries_to_purge) !== -1) {
|
||||
$(row).find('button.purge-library[data-id="' + rowData['section_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
if ($.inArray(rowData['row_id'], libraries_to_purge) !== -1) {
|
||||
$(row).find('button.purge-library[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,11 +279,11 @@ $('#libraries_list_table').on('click', 'td.edit-control > .edit-library-toggles
|
||||
var row = libraries_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var index_delete = $.inArray(rowData['section_id'], libraries_to_delete);
|
||||
var index_purge = $.inArray(rowData['section_id'], libraries_to_purge);
|
||||
var index_delete = $.inArray(rowData['row_id'], libraries_to_delete);
|
||||
var index_purge = $.inArray(rowData['row_id'], libraries_to_purge);
|
||||
|
||||
if (index_delete === -1) {
|
||||
libraries_to_delete.push(rowData['section_id']);
|
||||
libraries_to_delete.push(rowData['row_id']);
|
||||
if (index_purge === -1) {
|
||||
tr.find('button.purge-library').click();
|
||||
}
|
||||
@@ -311,11 +302,11 @@ $('#libraries_list_table').on('click', 'td.edit-control > .edit-library-toggles
|
||||
var row = libraries_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var index_delete = $.inArray(rowData['section_id'], libraries_to_delete);
|
||||
var index_purge = $.inArray(rowData['section_id'], libraries_to_purge);
|
||||
var index_delete = $.inArray(rowData['row_id'], libraries_to_delete);
|
||||
var index_purge = $.inArray(rowData['row_id'], libraries_to_purge);
|
||||
|
||||
if (index_purge === -1) {
|
||||
libraries_to_purge.push(rowData['section_id']);
|
||||
libraries_to_purge.push(rowData['row_id']);
|
||||
} else {
|
||||
libraries_to_purge.splice(index_purge, 1);
|
||||
if (index_delete != -1) {
|
||||
|
@@ -78,43 +78,43 @@ media_info_table_options = {
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'show') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'season') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Season"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 500, 280, null, null, null, 'art') + '" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'artist') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Artist"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'album') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Album"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'photo_album') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
} else if (rowData['media_type'] === 'photo') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
} else if (rowData['media_type'] === 'clip') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">' + rowData['title'] + '</span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 500, 280, null, null, null, 'art') + '" data-height="80" data-width="140">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
|
@@ -51,9 +51,9 @@ sync_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['user_id']) {
|
||||
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
@@ -67,7 +67,7 @@ sync_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['rating_key']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
|
@@ -82,29 +82,37 @@ user_ip_table_options = {
|
||||
"data": "last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var icon = '';
|
||||
var icon_title = '';
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
|
||||
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "30%",
|
||||
@@ -159,7 +167,7 @@ $('.user_ip_table').on('click', 'td.modal-control', function () {
|
||||
function showStreamDetails() {
|
||||
$.ajax({
|
||||
url: 'get_stream_data',
|
||||
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||
data: { row_id: rowData['history_row_id'], user: rowData['friendly_name'] },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
|
@@ -44,8 +44,8 @@ users_list_table_options = {
|
||||
"data": null,
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
$(td).html('<div class="edit-user-toggles">' +
|
||||
'<button class="btn btn-xs btn-warning delete-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||
'<button class="btn btn-xs btn-warning purge-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||
'<button class="btn btn-xs btn-warning delete-user" data-id="' + rowData['row_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||
'<button class="btn btn-xs btn-warning purge-user" data-id="' + rowData['row_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||
'<input type="checkbox" id="keep_history-' + rowData['user_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label> ' +
|
||||
'<input type="checkbox" id="allow_guest-' + rowData['user_id'] + '" name="allow_guest" value="1" ' + rowData['allow_guest'] + '><label class="edit-tooltip" for="allow_guest-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle Guest Access"><i class="fa fa-unlock-alt fa-lg fa-fw"></i></label> ' +
|
||||
'</div>');
|
||||
@@ -59,10 +59,12 @@ users_list_table_options = {
|
||||
"targets": [1],
|
||||
"data": "user_thumb",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
var inactive = '';
|
||||
if (!rowData['is_active']) { inactive = '<span class="inactive-user-tooltip" data-toggle="tooltip" title="User not on Plex server"><i class="fa fa-exclamation-triangle"></i></span>'; }
|
||||
if (cellData === '') {
|
||||
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);"></div></a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);">' + inactive + '</div></a>');
|
||||
} else {
|
||||
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');"></div></a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');">' + inactive + '</div></a>');
|
||||
}
|
||||
},
|
||||
"orderable": false,
|
||||
@@ -75,8 +77,8 @@ users_list_table_options = {
|
||||
"data": "friendly_name",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '">' +
|
||||
'<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>' +
|
||||
$(td).html('<div class="edit-user-name" data-id="' + rowData['row_id'] + '">' +
|
||||
'<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>' +
|
||||
'<input type="text" class="hidden" value="' + cellData + '">' +
|
||||
'</div>');
|
||||
} else {
|
||||
@@ -157,26 +159,34 @@ users_list_table_options = {
|
||||
"data":"last_played",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
var icon = '';
|
||||
var icon_title = '';
|
||||
var parent_info = '';
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
|
||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'episode') {
|
||||
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
|
||||
icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
|
||||
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' · E' + rowData['media_index'] + ')'; }
|
||||
else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
@@ -246,10 +256,10 @@ users_list_table_options = {
|
||||
},
|
||||
"rowCallback": function (row, rowData) {
|
||||
if ($.inArray(rowData['user_id'], users_to_delete) !== -1) {
|
||||
$(row).find('button.delete-user[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(row).find('button.delete-user[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
}
|
||||
if ($.inArray(rowData['user_id'], users_to_purge) !== -1) {
|
||||
$(row).find('button.purge-user[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(row).find('button.purge-user[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +270,7 @@ $('#users_list_table').on('click', 'td.modal-control', function () {
|
||||
var rowData = row.data();
|
||||
|
||||
$.get('get_stream_data', {
|
||||
row_id: rowData['id'],
|
||||
row_id: rowData['history_row_id'],
|
||||
user: rowData['friendly_name']
|
||||
}).then(function (jqXHR) {
|
||||
$("#info-modal").html(jqXHR);
|
||||
@@ -318,11 +328,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto
|
||||
var row = users_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
|
||||
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
|
||||
var index_delete = $.inArray(rowData['row_id'], users_to_delete);
|
||||
var index_purge = $.inArray(rowData['row_id'], users_to_purge);
|
||||
|
||||
if (index_delete === -1) {
|
||||
users_to_delete.push(rowData['user_id']);
|
||||
users_to_delete.push(rowData['row_id']);
|
||||
if (index_purge === -1) {
|
||||
tr.find('button.purge-user').click();
|
||||
}
|
||||
@@ -341,11 +351,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto
|
||||
var row = users_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
|
||||
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
|
||||
var index_delete = $.inArray(rowData['row_id'], users_to_delete);
|
||||
var index_purge = $.inArray(rowData['row_id'], users_to_purge);
|
||||
|
||||
if (index_purge === -1) {
|
||||
users_to_purge.push(rowData['user_id']);
|
||||
users_to_purge.push(rowData['row_id']);
|
||||
} else {
|
||||
users_to_purge.splice(index_purge, 1);
|
||||
if (index_delete != -1) {
|
||||
|
@@ -116,14 +116,14 @@
|
||||
});
|
||||
|
||||
if (libraries_to_delete.length > 0) {
|
||||
$('#libraries-to-delete').prepend('<p>Are you REALLY sure you want to delete the following libraries:</p>')
|
||||
$('#libraries-to-delete').prepend('<p>Are you REALLY sure you want to delete the following libraries:</p>');
|
||||
for (var i = 0; i < libraries_to_delete.length; i++) {
|
||||
$('#libraries-to-delete').append('<li>' + $('div[data-id=' + libraries_to_delete[i] + ']').text() + '</li>');
|
||||
}
|
||||
}
|
||||
|
||||
if (libraries_to_purge.length > 0) {
|
||||
$('#libraries-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following libraries:</p>')
|
||||
$('#libraries-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following libraries:</p>');
|
||||
for (var i = 0; i < libraries_to_purge.length; i++) {
|
||||
$('#libraries-to-purge').append('<li>' + $('div[data-id=' + libraries_to_purge[i] + ']').text() + '</li>');
|
||||
}
|
||||
@@ -131,33 +131,30 @@
|
||||
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
libraries_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_library',
|
||||
type: 'POST',
|
||||
data: { section_id: row },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "Library deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_all_library_history',
|
||||
type: 'POST',
|
||||
data: { row_ids: libraries_to_purge.join(',') },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "Library history purged";
|
||||
showMsg(msg, false, true, 2000);
|
||||
libraries_list_table.draw();
|
||||
}
|
||||
});
|
||||
libraries_to_purge.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_all_library_history',
|
||||
type: 'POST',
|
||||
data: { section_id: row },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "Library history purged";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_library',
|
||||
type: 'POST',
|
||||
data: { row_ids: libraries_to_delete.join(',') },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "Library deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
libraries_list_table.draw();
|
||||
}
|
||||
});
|
||||
libraries_list_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,7 +185,7 @@
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
if (result.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||
libraries_list_table.draw();
|
||||
} else {
|
||||
|
@@ -35,10 +35,14 @@ DOCUMENTATION :: END
|
||||
|
||||
<%def name="body()">
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.common import LIVE_TV_SECTION_ID
|
||||
from plexpy.helpers import page
|
||||
%>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
% if data['library_art']:
|
||||
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['library_art']}&width=1920&height=1080)"></div>
|
||||
<div class="art-face" style="background-image:url(${page('pms_image_proxy', data['library_art'], None, 1920, 1080)})"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -57,10 +61,22 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-back">
|
||||
<div class="user-info-wrapper">
|
||||
% if data['library_thumb'][:4] == 'http' or data['library_thumb'][:10] == 'interfaces':
|
||||
<div class="library-info-poster-face" style="background-image: url(${data['library_thumb']});"></div>
|
||||
% if data['library_thumb'].startswith('http'):
|
||||
<div class="library-info-poster-face" style="background-image: url(${page('pms_image_proxy', data['library_thumb'], None, 80, 80)});">
|
||||
% if not data['is_active']:
|
||||
<span class="inactive-library-tooltip" data-toggle="tooltip" title="Library not on Plex server">
|
||||
<i class="fa fa-2x fa-exclamation-triangle"></i>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
% else:
|
||||
<div class="library-info-poster-face svg-icon library-${data['section_type']}"></div>
|
||||
<div class="library-info-poster-face svg-icon library-${data['section_type']}">
|
||||
% if not data['is_active']:
|
||||
<span class="inactive-library-tooltip" data-toggle="tooltip" title="Library not on Plex server">
|
||||
<i class="fa fa-2x fa-exclamation-triangle"></i>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
<div class="user-info-username">
|
||||
<span class="set-username">${data['section_name']}</span>
|
||||
@@ -75,8 +91,10 @@ DOCUMENTATION :: END
|
||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
% if _session['user_group'] == 'admin':
|
||||
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
||||
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
|
||||
% endif
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,6 +161,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -168,6 +187,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-history">
|
||||
<div class="container-fluid">
|
||||
@@ -348,6 +368,7 @@ DOCUMENTATION :: END
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
% if data:
|
||||
<% from plexpy.common import LIVE_TV_SECTION_ID %>
|
||||
<script>
|
||||
% if str(data['section_id']).isdigit():
|
||||
var section_id = ${data['section_id']};
|
||||
@@ -402,6 +423,8 @@ DOCUMENTATION :: END
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
$(".inactive-library-tooltip").tooltip();
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
function loadMediaInfoTable() {
|
||||
// Build media info table
|
||||
@@ -462,17 +485,15 @@ DOCUMENTATION :: END
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
@@ -526,7 +547,9 @@ DOCUMENTATION :: END
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
||||
recentlyAdded();
|
||||
% endif
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
|
@@ -31,6 +31,9 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
%>
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-added-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
@@ -38,19 +41,19 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
% if item['media_type'] == 'episode' or item['media_type'] == 'movie':
|
||||
% if item['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">
|
||||
% elif item['media_type'] == 'episode':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['grandparent_title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['grandparent_title']}">
|
||||
% endif
|
||||
<div class="dashboard-recent-media-poster">
|
||||
% if item['media_type'] == 'episode':
|
||||
% if item['parent_thumb']:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['parent_thumb'], item['parent_rating_key'], 300, 450, fallback='poster')});">
|
||||
% else:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['grandparent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['grandparent_thumb'], item['grandparent_rating_key'], 300, 450, fallback='poster')});">
|
||||
% endif
|
||||
% elif item['media_type'] == 'movie':
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
|
||||
% endif
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
@@ -68,27 +71,27 @@ DOCUMENTATION :: END
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'episode':
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
<a href="${page('info', item['grandparent_rating_key'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
· <a href="info?rating_key=${item['rating_key']}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
· <a href="${page('info', item['rating_key'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
</h3>
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
<h3 class="text-muted"> </h3>
|
||||
% endif
|
||||
</div>
|
||||
% elif item['media_type'] == 'album':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
@@ -100,10 +103,10 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a class="text-muted" href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a class="text-muted" href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted"> </h3>
|
||||
</div>
|
||||
|
@@ -25,6 +25,8 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
|
||||
types = ('movie', 'show', 'artist', 'photo')
|
||||
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
|
||||
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
|
||||
@@ -33,10 +35,17 @@ DOCUMENTATION :: END
|
||||
%>
|
||||
% for section_type in types:
|
||||
% if section_type in data:
|
||||
<%
|
||||
row0 = data[section_type][0]
|
||||
%>
|
||||
<div class="dashboard-stats-instance" id="library-stats-instance-${section_type}" data-section_type="${section_type}">
|
||||
<div class="dashboard-stats-container">
|
||||
<div id="library-stats-background-${section_type}" class="dashboard-stats-background" style="background-image: url(pms_image_proxy?img=/:/resources/${section_type}-fanart.jpg&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art);">
|
||||
<div id="library-stats-background-${section_type}" class="dashboard-stats-background" style="background-image: url(${page('pms_image_proxy', row0['art'], None, 500, 280, 40, '282828', 3, fallback='art')});">
|
||||
% if row0['thumb'].startswith('http'):
|
||||
<div id="library-stats-thumb-${section_type}" class="dashboard-stats-flat hidden-xs" style="background-image: url(${page('pms_image_proxy', row0['thumb'], None, 80, 80)});"></div>
|
||||
% else:
|
||||
<div id="library-stats-thumb-${section_type}" class="dashboard-stats-flat svg-icon library-${section_type} hidden-xs"></div>
|
||||
% endif
|
||||
<div class="dashboard-stats-info-container">
|
||||
<div id="library-stats-title-${section_type}" class="dashboard-stats-info-title">
|
||||
<h4>${headers[section_type][0]}</h4>
|
||||
@@ -46,10 +55,11 @@ DOCUMENTATION :: END
|
||||
<div class="dashboard-stats-info scoller-content">
|
||||
<ul class="list-unstyled dashboard-stats-info-list">
|
||||
% for section in data[section_type]:
|
||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}">
|
||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${section_type}"
|
||||
data-art="${section.get('art')}" data-thumb="${section.get('thumb')}">
|
||||
<div class="sub-list">${loop.index + 1}</div>
|
||||
<div class="sub-value">
|
||||
<a href="library?section_id=${section['section_id']}" title="${section['section_name']}">
|
||||
<a href="${page('library', section['section_id'])}" title="${section['section_name']}">
|
||||
${section['section_name']}
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -19,16 +19,17 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data:
|
||||
<% from plexpy.helpers import page %>
|
||||
% for a in data:
|
||||
<ul class="list-unstyled">
|
||||
<div class="user-player-instance">
|
||||
<li>
|
||||
% if a['user_id']:
|
||||
<a href="user?user_id=${a['user_id']}" title="${a['friendly_name']}">
|
||||
<a href="${page('user', a['user_id'])}" title="${a['friendly_name']}">
|
||||
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>
|
||||
</a>
|
||||
<div class=" user-player-instance-name">
|
||||
<a href="user?user_id=${a['user_id']}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||
<a href="${page('user', a['user_id'])}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||
</div>
|
||||
% else:
|
||||
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>
|
||||
|
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials>
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
@@ -183,4 +183,4 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@@ -8,6 +8,9 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Tautulli - ${title} | ${server_name}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
|
@@ -31,6 +31,9 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
%>
|
||||
% if data:
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-added-row-scroller" style="left: 0;">
|
||||
@@ -39,9 +42,9 @@ DOCUMENTATION :: END
|
||||
<div class="dashboard-recent-media-instance">
|
||||
<li data-type="${item['media_type']}">
|
||||
% if item['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
@@ -57,15 +60,15 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
<h3 class="text-muted"> </h3>
|
||||
</div>
|
||||
% elif item['media_type'] == 'show':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
@@ -81,7 +84,7 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
${item['child_count']} Seasons
|
||||
@@ -89,9 +92,13 @@ DOCUMENTATION :: END
|
||||
<h3 class="text-muted"> </h3>
|
||||
</div>
|
||||
% elif item['media_type'] == 'season':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['parent_title']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb'] or item['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
% if item['thumb']:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
|
||||
% else:
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['parent_thumb'], item['parent_rating_key'], 300, 450, fallback='poster')});">
|
||||
% endif
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
@@ -107,17 +114,17 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a class="text-muted" href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a class="text-muted" href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted"> </h3>
|
||||
</div>
|
||||
% elif item['media_type'] == 'episode':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['grandparent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['grandparent_thumb'], item['grandparent_rating_key'], 300, 450, fallback='poster')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
@@ -133,21 +140,21 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
<a href="${page('info', item['grandparent_rating_key'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
·
|
||||
<a href="info?rating_key=${item['rating_key']}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
<a href="${page('info', item['rating_key'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% elif item['media_type'] == 'album':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}">
|
||||
<a href="${page('info', item['rating_key'])}" title="${item['parent_title']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||
<script>
|
||||
@@ -163,10 +170,10 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a class="text-muted" href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a class="text-muted" href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted"> </h3>
|
||||
</div>
|
||||
|
@@ -217,7 +217,7 @@
|
||||
<div id="git_update_options">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']}> Update Automatically
|
||||
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
|
||||
</label>
|
||||
<p class="help-block">Update Tautulli automatically if an update is available.</p>
|
||||
</div>
|
||||
@@ -265,6 +265,20 @@
|
||||
</div>
|
||||
<p class="help-block">Optional: The path to your git environment variable. Leave blank for default.</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label>Repair Git Install</label>
|
||||
<p class="help-block">
|
||||
Attempt to fix updating by resetting your Tautulli installation back to <strong>${common.RELEASE}</strong>.<br />
|
||||
Note: This will not affect any saved history or settings.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-form" type="button" id="reset_git_install">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||
@@ -984,6 +998,20 @@
|
||||
</div>
|
||||
<p class="help-block">Set the delay (in seconds) to wait for consecutive recently added items to group together and to allow metadata to be processed before sending the notification. Minimum 60 seconds.</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label>Flush Recently Added</label>
|
||||
<p class="help-block">
|
||||
Attempt to fix recently added notifications by flushing out all of the recently added items in the database.<br />
|
||||
Warning: This will reset all recently added notifications. For emergency use only when recently added notifications are stuck!
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-form" type="button" id="delete_recently_added">Flush</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_recently_added_upgrade" id="notify_recently_added_upgrade" value="1" ${config['notify_recently_added_upgrade']}> Send a Notification for New Versions <span class="settings-warning">[Not working]</span>
|
||||
@@ -1225,6 +1253,19 @@
|
||||
</label>
|
||||
<p class="help-block">Enable to lookup links to MusicBrainz for music when available.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="maxmind_license_key">Delete Lookup Info</label>
|
||||
<p class="help-block">Delete all cached metadata lookup info in Tautulli.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-form delete_all_lookups" type="button" data-service="themoviedb">TheMovieDB</button>
|
||||
<button class="btn btn-form delete_all_lookups" type="button" data-service="tvmaze">TVmaze</button>
|
||||
<button class="btn btn-form delete_all_lookups" type="button" data-service="musicbrainz">MusicBrainz</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Geolocation Database</h3>
|
||||
@@ -1242,7 +1283,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
Enter and save your MaxMind License Key to install the GeoLite2 database.
|
||||
Enter your MaxMind License Key to install the GeoLite2 database.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -1250,16 +1291,17 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="geoip_db" name="geoip_db" value="${config['geoip_db']}" ${docker_setting}>
|
||||
<input type="text" class="form-control" id="geoip_db" name="geoip_db" value="${config['geoip_db']}" ${docker_setting} data-parsley-trigger="change" data-parsley-pattern=".+\.mmdb$" data-parsley-errors-container="#geoip_db_error" data-parsley-error-message="Must end with '.mmdb'">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-form" type="button" id="install_geoip_db" disabled>${'Update' if config["geoip_db_installed"] else 'Install'}</button>
|
||||
<button class="btn btn-form" type="button" id="uninstall_geoip_db">Uninstall</button>
|
||||
<button class="btn btn-form" type="button" id="install_geoip_db">${'Update' if config["geoip_db_installed"] else 'Install'}</button>
|
||||
<button class="btn btn-form" type="button" id="uninstall_geoip_db" ${'disabled' if not config['geoip_db_installed'] else ''}>Uninstall</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="geoip_db_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
GeoLite2 Database last updated <strong><span id="geoip_db_updated">never</span></strong>.
|
||||
Leave blank to install in the default location. GeoLite2 database last updated <strong><span id="geoip_db_updated">never</span></strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
@@ -1979,12 +2021,10 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
function preSaveChecks(_callback) {
|
||||
if (serverChanged) {
|
||||
verifyServer();
|
||||
}
|
||||
verifyPMSWebURL();
|
||||
|
||||
if (_callback) {
|
||||
if (serverChanged) {
|
||||
verifyServer(_callback);
|
||||
} else if (typeof _callback === "function") {
|
||||
_callback();
|
||||
}
|
||||
}
|
||||
@@ -2001,7 +2041,6 @@ $(document).ready(function() {
|
||||
getNewslettersTable();
|
||||
getMobileDevicesTable();
|
||||
loadUpdateDistros();
|
||||
enableGeoLiteInstallButton();
|
||||
settingsChanged = false;
|
||||
}
|
||||
|
||||
@@ -2010,12 +2049,13 @@ $(document).ready(function() {
|
||||
settingsChanged = true;
|
||||
});
|
||||
|
||||
function saveSettings() {
|
||||
function saveSettings(showMsg, _callback) {
|
||||
if (configForm.parsley().validate()) {
|
||||
doAjaxCall('configUpdate', $(this), 'tabs', true, true, postSaveChecks);
|
||||
return false;
|
||||
doAjaxCall('configUpdate', $(this), 'tabs', true, showMsg, _callback);
|
||||
return true;
|
||||
} else {
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Please verify your settings.', false, true, 5000, true)
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Please verify your settings.', false, true, 5000, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2029,7 +2069,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
$('.save-button').click(function() {
|
||||
preSaveChecks(function () { saveSettings() });
|
||||
preSaveChecks(function () { saveSettings(true, postSaveChecks) });
|
||||
});
|
||||
|
||||
initConfigCheckbox('#api_enabled');
|
||||
@@ -2130,11 +2170,17 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
$("#delete_temp_sessions").click(function () {
|
||||
var msg = 'Are you sure you want to flush the temporary sessions?<br /><strong>This will reset all currently active sessions.</strong>';
|
||||
var msg = 'Are you sure you want to flush the temporary sessions?<br /><br /><strong>This will reset all currently active sessions.</strong>';
|
||||
var url = 'delete_temp_sessions';
|
||||
confirmAjaxCall(url, msg);
|
||||
});
|
||||
|
||||
$("#delete_recently_added").click(function () {
|
||||
var msg = 'Are you sure you want to flush the recently added items?<br /><br /><strong>This will reset all recently added notifications.</strong>';
|
||||
var url = 'delete_recently_added';
|
||||
confirmAjaxCall(url, msg);
|
||||
});
|
||||
|
||||
$("#switch_git_branch").click(function () {
|
||||
var current_remote = "${config['git_remote']}";
|
||||
var current_branch = "${config['git_branch']}";
|
||||
@@ -2155,6 +2201,17 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#reset_git_install").click(function () {
|
||||
var msg = 'Are you sure you want to reset your Tautulli installtion back to <strong>${common.RELEASE}</strong>?' +
|
||||
'<br /><br />Tautulli will restart.';
|
||||
$('#confirm-message').html(msg);
|
||||
$('#confirm-modal').modal();
|
||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||
settingsChanged = false;
|
||||
window.location.href = 'reset_git_install';
|
||||
});
|
||||
});
|
||||
|
||||
$('#api_key').click(function(){ $('#api_key').select() });
|
||||
$("#generate_api").click(function() {
|
||||
$.get('generate_api_key',
|
||||
@@ -2338,6 +2395,7 @@ $(document).ready(function() {
|
||||
|
||||
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
||||
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||
showMsg('Verifying Plex server...', true, true, 10000, false);
|
||||
$.ajax({
|
||||
url: 'get_server_id',
|
||||
data: {
|
||||
@@ -2375,10 +2433,11 @@ $(document).ready(function() {
|
||||
} else {
|
||||
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").removeClass("has-error");
|
||||
showMsg('<i class="fa fa-check"></i> Server verified.', false, true, 5000);
|
||||
serverChanged = false;
|
||||
}
|
||||
|
||||
if (_callback) {
|
||||
if (typeof _callback === "function") {
|
||||
_callback();
|
||||
}
|
||||
} else {
|
||||
@@ -2530,6 +2589,7 @@ $(document).ready(function() {
|
||||
for (var i in libraries_list) {
|
||||
var title = libraries_list[i].section_name;
|
||||
var key = libraries_list[i].section_id;
|
||||
if (key === 999999) { continue; } // Don't show Live TV library
|
||||
$('#sortable_home_library_cards').append(
|
||||
'<li class="card card-sortable">' +
|
||||
'<div class="card-handle"><i class="fa fa-bars"></i></div>' +
|
||||
@@ -2779,12 +2839,23 @@ $(document).ready(function() {
|
||||
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!';
|
||||
'<br /><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);
|
||||
});
|
||||
|
||||
$('body').on('click', '.delete_all_lookups', function () {
|
||||
var service = $(this).data('service');
|
||||
var name = $(this).text();
|
||||
|
||||
var msg = 'Are you sure you want to delete all the metadata lookup info from <strong>' + name + '</strong>?' +
|
||||
'<br /><br />Tautulli will lookup the metadata info again the next time a notification is sent.';
|
||||
var url = 'delete_lookup_info';
|
||||
var data = { service: service, delete_all: true };
|
||||
confirmAjaxCall(url, msg, data, false);
|
||||
});
|
||||
|
||||
function baseURLSet() {
|
||||
if ($('#http_base_url').val()) {
|
||||
$('.base-url-warning').hide();
|
||||
@@ -2862,20 +2933,24 @@ $(document).ready(function() {
|
||||
gotoSetting('3rd_party_apis', 'geoip_db')
|
||||
}
|
||||
|
||||
function enableGeoLiteInstallButton() {
|
||||
$('#install_geoip_db').prop('disabled', !(Boolean($("#maxmind_license_key").val())));
|
||||
}
|
||||
enableGeoLiteInstallButton();
|
||||
|
||||
if ("${config['geoip_db_installed']}" > "0") {
|
||||
$("#geoip_db_updated").text(moment("${config['geoip_db_installed']}", "X").fromNow());
|
||||
}
|
||||
|
||||
$("#install_geoip_db").click(function () {
|
||||
var maxmind_license_key = $("#maxmind_license_key");
|
||||
maxmind_license_key.val($.trim(maxmind_license_key.val()));
|
||||
if (maxmind_license_key.val() === "") {
|
||||
maxmind_license_key.focus();
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Maxmind License Key is required.', false, true, 5000, true);
|
||||
return false;
|
||||
} else if (!(saveSettings())) {
|
||||
return false;
|
||||
}
|
||||
var msg = 'Are you sure you want to install the GeoLite2 database?<br /><br />' +
|
||||
'The database is used to lookup IP address geolocation info.<br />' +
|
||||
'The database will be downloaded from <a href="${anon_url("https://dev.maxmind.com/geoip/geoip2/geolite2/")}" target="_blank">MaxMind</a>, <br />' +
|
||||
'and requires <strong>100MB</strong> of free space to install in your Tautulli directory.<br />';
|
||||
'and requires <strong>100MB</strong> of free space to install.<br />';
|
||||
var url = 'install_geoip_db';
|
||||
if ($(this).text() === 'Update') {
|
||||
url += '?update=true';
|
||||
@@ -2883,6 +2958,7 @@ $(document).ready(function() {
|
||||
confirmAjaxCall(url, msg, null, 'Installing GeoLite2 database.', function (result) {
|
||||
if (result.result === "success") {
|
||||
$('#install_geoip_db').text('Update');
|
||||
$('#uninstall_geoip_db').prop('disabled', false);
|
||||
$('#geoip_db_updated').text(moment(result.updated, "X").fromNow());
|
||||
}
|
||||
getSchedulerTable();
|
||||
@@ -2896,6 +2972,7 @@ $(document).ready(function() {
|
||||
confirmAjaxCall(url, msg, null, 'Uninstalling GeoLite2 database.', function (result) {
|
||||
if (result.result === "success") {
|
||||
$('#install_geoip_db').text('Install');
|
||||
$('#uninstall_geoip_db').prop('disabled', true);
|
||||
$('#geoip_db_updated').text('never');
|
||||
}
|
||||
getSchedulerTable();
|
||||
|
@@ -178,6 +178,11 @@ DOCUMENTATION :: END
|
||||
<td>${data['stream_video_framerate']}</td>
|
||||
<td>${data['video_framerate']}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dynamic Range</td>
|
||||
<td>${data['stream_video_dynamic_range']}</td>
|
||||
<td>${data['video_dynamic_range']}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aspect Ratio</td>
|
||||
<td>-</td>
|
||||
|
@@ -39,30 +39,43 @@ DOCUMENTATION :: END
|
||||
<ul class="list-unstyled breadcrumb">
|
||||
% if query['media_type'] == 'movie':
|
||||
<li>Movies</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">${query['title']}</li>
|
||||
% elif query['media_type'] == 'show':
|
||||
<li>TV Shows</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">${query['grandparent_title']}</li>
|
||||
% elif query['media_type'] == 'season':
|
||||
<li class="hidden-xs hidden-sm">TV Shows</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">Season ${query['parent_media_index']}</li>
|
||||
% elif query['media_type'] == 'episode':
|
||||
<li class="hidden-xs hidden-sm">TV Shows</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li>Season ${query['parent_media_index']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">Episode ${query['media_index']} - ${query['title']}</li>
|
||||
% elif query['media_type'] == 'artist':
|
||||
<li><Music</li>
|
||||
<li>Music</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">${query['grandparent_title']}</li>
|
||||
% elif query['media_type'] == 'album':
|
||||
<li class="hidden-xs hidden-sm">Music</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li>${query['grandparent_title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">${query['parent_title']}</li>
|
||||
% elif query['media_type'] == 'track':
|
||||
<li class="hidden-xs hidden-sm">Music</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li>${query['parent_title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active">Track ${query['media_index']} - ${query['title']}</li>
|
||||
% endif
|
||||
</ul>
|
||||
@@ -127,6 +140,7 @@ DOCUMENTATION :: END
|
||||
</%def>
|
||||
|
||||
<%def name="modalIncludes()">
|
||||
% if query:
|
||||
<div class="modal fade" id="confirm-modal-update" tabindex="-1" role="dialog" aria-labelledby="confirm-modal-update">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -169,6 +183,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
|
@@ -51,7 +51,13 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-back">
|
||||
<div class="user-info-wrapper">
|
||||
<div class="user-info-poster-face" style="background-image: url(${data['user_thumb']});"></div>
|
||||
<div class="user-info-poster-face" style="background-image: url(${data['user_thumb']});">
|
||||
% if not data['is_active']:
|
||||
<span class="inactive-user-tooltip" data-toggle="tooltip" title="User not on Plex server">
|
||||
<i class="fa fa-2x fa-exclamation-triangle"></i>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="user-info-username">
|
||||
<span class="set-username">${data['friendly_name']}</span>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -168,6 +174,9 @@ DOCUMENTATION :: END
|
||||
<label class="btn btn-dark">
|
||||
<input type="radio" name="media_type-filter" id="history-track" value="track" autocomplete="off"> Music
|
||||
</label>
|
||||
<label class="btn btn-dark">
|
||||
<input type="radio" name="media_type-filter" id="history-live" value="live" autocomplete="off"> Live TV
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
|
||||
@@ -537,6 +546,8 @@ DOCUMENTATION :: END
|
||||
login_log_table.draw();
|
||||
});
|
||||
|
||||
$(".inactive-user-tooltip").tooltip();
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
$("#edit-user-tooltip").tooltip();
|
||||
|
||||
@@ -563,17 +574,15 @@ DOCUMENTATION :: END
|
||||
$('#deleteType').text('history');
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
|
@@ -27,6 +27,9 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
%>
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-watched-row-scroller" style="left: 0;">
|
||||
<ul class="dashboard-recent-media list-unstyled">
|
||||
@@ -35,12 +38,12 @@ DOCUMENTATION :: END
|
||||
% if item['media_type'] == 'episode' or item['media_type'] == 'movie':
|
||||
% if item['rating_key']:
|
||||
% if item['media_type'] == 'movie':
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">
|
||||
% elif item['media_type'] == 'episode':
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['grandparent_title']}">
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['grandparent_title']}">
|
||||
% endif
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||
<script>
|
||||
@@ -56,19 +59,38 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['media_type'] == 'episode':
|
||||
% if item['live']:
|
||||
<h3>
|
||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted" title="${item['title']}">
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
% if item['media_index']:
|
||||
<h3 class="text-muted">
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
· <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
</h3>
|
||||
% else:
|
||||
<h3 class="text-muted">
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['originally_available_at']}">${item['originally_available_at']}</a>
|
||||
</h3>
|
||||
% endif
|
||||
% else:
|
||||
<h3>
|
||||
<a href="${page('info', item['grandparent_rating_key'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted" title="${item['title']}">
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
· <a href="info?source=history&rating_key=${item['rating_key']}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
|
||||
· <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
|
||||
</h3>
|
||||
% endif
|
||||
% elif item['media_type'] == 'movie':
|
||||
<h3 title="${item['title']}">
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">${item['year']}</h3>
|
||||
<h3 class="text-muted"> </h3>
|
||||
@@ -94,9 +116,9 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
% elif item['media_type'] == 'track':
|
||||
% if item['rating_key']:
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['parent_title']}">
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['parent_title']}">
|
||||
<div class="dashboard-recent-media-cover">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||
<script>
|
||||
@@ -109,13 +131,13 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3 title="${item['original_title'] or item['grandparent_title']}">
|
||||
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['original_title'] or item['grandparent_title']}">${item['original_title'] or item['grandparent_title']}</a>
|
||||
<a href="${page('info', item['grandparent_rating_key'])}" title="${item['original_title'] or item['grandparent_title']}">${item['original_title'] or item['grandparent_title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted" title="${item['title']}">
|
||||
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>
|
||||
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">
|
||||
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
<a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% else:
|
||||
|
@@ -119,14 +119,14 @@
|
||||
});
|
||||
|
||||
if (users_to_delete.length > 0) {
|
||||
$('#users-to-delete').prepend('<p>Are you REALLY sure you want to delete and purge all history for the following users:</p>')
|
||||
$('#users-to-delete').prepend('<p>Are you REALLY sure you want to delete and purge all history for the following users:</p>');
|
||||
for (var i = 0; i < users_to_delete.length; i++) {
|
||||
$('#users-to-delete').append('<li>' + $('div[data-id=' + users_to_delete[i] + '] > input').val() + '</li>');
|
||||
}
|
||||
}
|
||||
|
||||
if (users_to_purge.length > 0) {
|
||||
$('#users-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following users:</p>')
|
||||
$('#users-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following users:</p>');
|
||||
for (var i = 0; i < users_to_purge.length; i++) {
|
||||
$('#users-to-purge').append('<li>' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '</li>');
|
||||
}
|
||||
@@ -134,33 +134,30 @@
|
||||
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
users_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_user',
|
||||
type: 'POST',
|
||||
data: { user_id: row },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "User deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_all_user_history',
|
||||
type: 'POST',
|
||||
data: { row_ids: users_to_purge.join(',') },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "User history purged";
|
||||
showMsg(msg, false, true, 2000);
|
||||
users_list_table.draw();
|
||||
}
|
||||
});
|
||||
users_to_purge.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_all_user_history',
|
||||
type: 'POST',
|
||||
data: { user_id: row },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "User history purged";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: 'delete_user',
|
||||
type: 'POST',
|
||||
data: { row_ids: users_to_delete.join(',') },
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "User deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
users_list_table.draw();
|
||||
}
|
||||
});
|
||||
users_list_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
if (result.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||
users_list_table.draw();
|
||||
} else {
|
||||
|
@@ -27,7 +27,7 @@
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
@@ -216,6 +216,7 @@
|
||||
<input type="checkbox" name="first_run" id="first_run" value="1" checked>
|
||||
<input type="checkbox" name="group_history_tables" id="group_history_tables" value="1" checked>
|
||||
<input type="checkbox" name="history_table_activity" id="history_table_activity" value="1" checked>
|
||||
<input type="checkbox" name="win_sys_tray" id="win_sys_tray" value="1" checked>
|
||||
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" checked>
|
||||
<input type="checkbox" name="api_enabled" id="api_enabled" value="1" checked>
|
||||
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" checked>
|
||||
@@ -223,6 +224,8 @@
|
||||
<input type="checkbox" name="check_github" id="check_github" value="1" checked>
|
||||
<input type="checkbox" name="log_blacklist" id="log_blacklist" value="1" checked>
|
||||
<input type="checkbox" name="cache_images" id="cache_images" value="1" checked>
|
||||
<input type="checkbox" name="notify_group_recently_added_grandparent" id="notify_group_recently_added_grandparent" value="1" checked>
|
||||
<input type="checkbox" name="notify_group_recently_added_parent" id="notify_group_recently_added_parent" value="1" checked>
|
||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" checked>
|
||||
<input type="checkbox" name="first_run_complete" id="first_run_complete" value="1" checked>
|
||||
<input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
|
||||
|
@@ -26,6 +26,7 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Tautulli Newsletter - ${subject}</title>
|
||||
<link rel="shortcut icon" href="${base_url_image + 'images/favicon/favicon.ico' if base_url_image else 'https://tautulli.com/images/favicon.ico'}">
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
@@ -694,7 +695,7 @@
|
||||
<div class="sub-header-count" style="margin-left: auto;margin-right: auto;font-size: 30px;text-align: center;">
|
||||
<span class="count" style="color: #E5A00D;">${len(recently_added['show'])}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">show${'s' if len(recently_added['show']) > 1 else ''}</span> /
|
||||
<% total_episodes = sum(season['episode_count'] for show in recently_added['show'] for season in show['season']) %>
|
||||
<span class="count" style="color: #E5A00D;">${total_episodes}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">episode${'s' if total > 1 else ''}</span>
|
||||
<span class="count" style="color: #E5A00D;">${total_episodes}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">episode${'s' if total_episodes > 1 else ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -847,7 +848,7 @@
|
||||
<div class="sub-header-count" style="margin-left: auto;margin-right: auto;font-size: 30px;text-align: center;">
|
||||
<span class="count" style="color: #E5A00D;">${len(recently_added['artist'])}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">artist${'s' if len(recently_added['artist']) > 1 else ''}</span> /
|
||||
<% total_albums = sum(artist['album_count'] for artist in recently_added['artist']) %>
|
||||
<span class="count" style="color: #E5A00D;">${total_albums}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">album${'s' if total > 1 else ''}</span>
|
||||
<span class="count" style="color: #E5A00D;">${total_albums}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">album${'s' if total_albums > 1 else ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@@ -26,6 +26,7 @@
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<title>Tautulli Newsletter - ${subject}</title>
|
||||
<link rel="shortcut icon" href="${base_url_image + 'images/favicon/favicon.ico' if base_url_image else 'https://tautulli.com/images/favicon.ico'}">
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
@@ -694,7 +695,7 @@
|
||||
<div class="sub-header-count">
|
||||
<span class="count">${len(recently_added['show'])}</span> <span class="count-units">show${'s' if len(recently_added['show']) > 1 else ''}</span> /
|
||||
<% total_episodes = sum(season['episode_count'] for show in recently_added['show'] for season in show['season']) %>
|
||||
<span class="count">${total_episodes}</span> <span class="count-units">episode${'s' if total > 1 else ''}</span>
|
||||
<span class="count">${total_episodes}</span> <span class="count-units">episode${'s' if total_episodes > 1 else ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -847,7 +848,7 @@
|
||||
<div class="sub-header-count">
|
||||
<span class="count">${len(recently_added['artist'])}</span> <span class="count-units">artist${'s' if len(recently_added['artist']) > 1 else ''}</span> /
|
||||
<% total_albums = sum(artist['album_count'] for artist in recently_added['artist']) %>
|
||||
<span class="count">${total_albums}</span> <span class="count-units">album${'s' if total > 1 else ''}</span>
|
||||
<span class="count">${total_albums}</span> <span class="count-units">album${'s' if total_albums > 1 else ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
from .core import where
|
||||
|
||||
__version__ = "2019.03.09"
|
||||
__version__ = "2019.11.28"
|
||||
|
@@ -771,36 +771,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
|
||||
+OkuE6N36B9K
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Class 2 Primary CA O=Certplus
|
||||
# Subject: CN=Class 2 Primary CA O=Certplus
|
||||
# Label: "Certplus Class 2 Primary CA"
|
||||
# Serial: 177770208045934040241468760488327595043
|
||||
# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b
|
||||
# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb
|
||||
# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
|
||||
PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
|
||||
cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
|
||||
MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
|
||||
IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
|
||||
ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
|
||||
VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
|
||||
kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
|
||||
EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
|
||||
H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
|
||||
HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
|
||||
DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
|
||||
QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
|
||||
Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
|
||||
AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
|
||||
yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
|
||||
FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
|
||||
ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
|
||||
kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
|
||||
l7+ijrRU
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
||||
# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
||||
# Label: "DST Root CA X3"
|
||||
@@ -1219,36 +1189,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
|
||||
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
|
||||
# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
|
||||
# Label: "Deutsche Telekom Root CA 2"
|
||||
# Serial: 38
|
||||
# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08
|
||||
# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf
|
||||
# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
|
||||
MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
|
||||
IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
|
||||
IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
|
||||
RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
|
||||
U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
|
||||
IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
|
||||
ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
|
||||
QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
|
||||
rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
|
||||
NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
|
||||
QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
|
||||
txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
|
||||
BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
|
||||
AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
|
||||
tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
|
||||
IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
|
||||
6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
|
||||
xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
|
||||
Cm26OWMohpLzGITY+9HPBVZkVw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
|
||||
# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
|
||||
# Label: "Cybertrust Global Root"
|
||||
@@ -3453,46 +3393,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
|
||||
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
|
||||
# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
|
||||
# Label: "Certinomis - Root CA"
|
||||
# Serial: 1
|
||||
# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f
|
||||
# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8
|
||||
# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET
|
||||
MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb
|
||||
BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz
|
||||
MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx
|
||||
FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g
|
||||
Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2
|
||||
fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl
|
||||
LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV
|
||||
WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF
|
||||
TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb
|
||||
5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc
|
||||
CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri
|
||||
wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ
|
||||
wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG
|
||||
m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4
|
||||
F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng
|
||||
WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB
|
||||
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0
|
||||
2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF
|
||||
AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/
|
||||
0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw
|
||||
F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS
|
||||
g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj
|
||||
qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN
|
||||
h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/
|
||||
ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V
|
||||
btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj
|
||||
Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ
|
||||
8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW
|
||||
gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
||||
# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
||||
# Label: "OISTE WISeKey Global Root GB CA"
|
||||
@@ -4656,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
|
||||
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
|
||||
mpv0
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||
# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||
# Label: "Entrust Root Certification Authority - G4"
|
||||
# Serial: 289383649854506086828220374796556676440
|
||||
# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88
|
||||
# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01
|
||||
# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw
|
||||
gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL
|
||||
Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg
|
||||
MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw
|
||||
BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0
|
||||
MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT
|
||||
MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1
|
||||
c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ
|
||||
bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg
|
||||
Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ
|
||||
2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E
|
||||
T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j
|
||||
5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM
|
||||
C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T
|
||||
DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX
|
||||
wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A
|
||||
2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm
|
||||
nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
|
||||
dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl
|
||||
N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj
|
||||
c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
|
||||
VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS
|
||||
5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS
|
||||
Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr
|
||||
hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/
|
||||
B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI
|
||||
AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw
|
||||
H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+
|
||||
b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk
|
||||
2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol
|
||||
IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
|
||||
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
||||
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
||||
-----END CERTIFICATE-----
|
||||
|
@@ -34,6 +34,8 @@ from apscheduler.triggers.interval import IntervalTrigger
|
||||
from UniversalAnalytics import Tracker
|
||||
import pytz
|
||||
|
||||
PYTHON_VERSION = sys.version_info[:3]
|
||||
|
||||
import activity_handler
|
||||
import activity_pinger
|
||||
import common
|
||||
@@ -65,7 +67,7 @@ SYS_LANGUAGE = None
|
||||
SYS_ENCODING = None
|
||||
|
||||
QUIET = False
|
||||
VERBOSE = True
|
||||
VERBOSE = False
|
||||
DAEMON = False
|
||||
CREATEPID = False
|
||||
PIDFILE = None
|
||||
@@ -122,6 +124,7 @@ def initialize(config_file):
|
||||
|
||||
global CONFIG
|
||||
global CONFIG_FILE
|
||||
global VERBOSE
|
||||
global _INITIALIZED
|
||||
global CURRENT_VERSION
|
||||
global LATEST_VERSION
|
||||
@@ -152,6 +155,8 @@ def initialize(config_file):
|
||||
if not log_writable and not QUIET:
|
||||
sys.stderr.write("Unable to create the log directory. Logging to screen only.\n")
|
||||
|
||||
VERBOSE = VERBOSE or bool(CONFIG.VERBOSE_LOGS)
|
||||
|
||||
# Start the logger, disable console if needed
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR if log_writable else None,
|
||||
verbose=VERBOSE)
|
||||
@@ -582,15 +587,18 @@ def dbcheck():
|
||||
'media_index INTEGER, parent_media_index INTEGER, '
|
||||
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, year INTEGER, '
|
||||
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||
'originally_available_at TEXT, added_at INTEGER, guid TEXT, '
|
||||
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
|
||||
'transcode_decision TEXT, container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, '
|
||||
'video_codec TEXT, video_bitrate INTEGER, video_resolution TEXT, video_width INTEGER, video_height INTEGER, '
|
||||
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
|
||||
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, '
|
||||
'video_dynamic_range TEXT, aspect_ratio TEXT, '
|
||||
'audio_codec TEXT, audio_bitrate INTEGER, audio_channels INTEGER, subtitle_codec TEXT, '
|
||||
'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile TEXT, '
|
||||
'stream_container_decision TEXT, stream_container TEXT, '
|
||||
'stream_video_decision TEXT, stream_video_codec TEXT, stream_video_bitrate INTEGER, stream_video_width INTEGER, '
|
||||
'stream_video_height INTEGER, stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
|
||||
'stream_video_dynamic_range TEXT, '
|
||||
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
|
||||
'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, '
|
||||
'transcode_protocol TEXT, transcode_container TEXT, '
|
||||
@@ -599,7 +607,8 @@ def dbcheck():
|
||||
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
|
||||
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
|
||||
'synced_version INTEGER, synced_version_profile TEXT, '
|
||||
'live INTEGER, live_uuid TEXT, secure INTEGER, relayed INTEGER, '
|
||||
'live INTEGER, live_uuid TEXT, channel_call_sign TEXT, channel_identifier TEXT, channel_thumb TEXT, '
|
||||
'secure INTEGER, relayed INTEGER, '
|
||||
'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)'
|
||||
)
|
||||
@@ -620,7 +629,7 @@ def dbcheck():
|
||||
'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, '
|
||||
'container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, video_bitrate INTEGER, video_bit_depth INTEGER, '
|
||||
'video_codec TEXT, video_codec_level TEXT, video_width INTEGER, video_height INTEGER, video_resolution TEXT, '
|
||||
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
|
||||
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, video_dynamic_range TEXT, aspect_ratio TEXT, '
|
||||
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
|
||||
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
|
||||
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
|
||||
@@ -630,7 +639,7 @@ def dbcheck():
|
||||
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
|
||||
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
|
||||
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
|
||||
'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
|
||||
'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, stream_video_dynamic_range TEXT, '
|
||||
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
|
||||
'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, '
|
||||
'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, '
|
||||
@@ -647,25 +656,26 @@ def dbcheck():
|
||||
'art TEXT, media_type TEXT, year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, '
|
||||
'last_viewed_at INTEGER, content_rating TEXT, summary TEXT, tagline TEXT, rating TEXT, '
|
||||
'duration INTEGER DEFAULT 0, guid TEXT, directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT, '
|
||||
'labels TEXT)'
|
||||
'labels TEXT, live INTEGER DEFAULT 0, channel_call_sign TEXT, channel_identifier TEXT, channel_thumb TEXT)'
|
||||
)
|
||||
|
||||
# users table :: This table keeps record of the friends list
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL, friendly_name TEXT, '
|
||||
'thumb TEXT, custom_avatar_url TEXT, email TEXT, is_admin INTEGER DEFAULT 0, is_home_user INTEGER DEFAULT NULL, '
|
||||
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
|
||||
'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0, allow_guest INTEGER DEFAULT 0, '
|
||||
'user_token TEXT, server_token TEXT, shared_libraries TEXT, filter_all TEXT, filter_movies TEXT, filter_tv TEXT, '
|
||||
'filter_music TEXT, filter_photos TEXT)'
|
||||
'thumb TEXT, custom_avatar_url TEXT, email TEXT, is_active INTEGER DEFAULT 1, is_admin INTEGER DEFAULT 0, '
|
||||
'is_home_user INTEGER DEFAULT NULL, is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, '
|
||||
'do_notify INTEGER DEFAULT 1, keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0, '
|
||||
'allow_guest INTEGER DEFAULT 0, user_token TEXT, server_token TEXT, shared_libraries TEXT, '
|
||||
'filter_all TEXT, filter_movies TEXT, filter_tv TEXT, filter_music TEXT, filter_photos TEXT)'
|
||||
)
|
||||
|
||||
# library_sections table :: This table keeps record of the servers library sections
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS library_sections (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'server_id TEXT, section_id INTEGER, section_name TEXT, section_type TEXT, agent TEXT, '
|
||||
'thumb TEXT, custom_thumb_url TEXT, art TEXT, count INTEGER, parent_count INTEGER, child_count INTEGER, '
|
||||
'thumb TEXT, custom_thumb_url TEXT, art TEXT, custom_art_url TEXT, '
|
||||
'count INTEGER, parent_count INTEGER, child_count INTEGER, is_active INTEGER DEFAULT 1, '
|
||||
'do_notify INTEGER DEFAULT 1, do_notify_created INTEGER DEFAULT 1, keep_history INTEGER DEFAULT 1, '
|
||||
'deleted_section INTEGER DEFAULT 0, UNIQUE(server_id, section_id))'
|
||||
)
|
||||
@@ -686,16 +696,19 @@ def dbcheck():
|
||||
'on_created INTEGER DEFAULT 0, on_extdown INTEGER DEFAULT 0, on_intdown INTEGER DEFAULT 0, '
|
||||
'on_extup INTEGER DEFAULT 0, on_intup INTEGER DEFAULT 0, on_pmsupdate INTEGER DEFAULT 0, '
|
||||
'on_concurrent INTEGER DEFAULT 0, on_newdevice INTEGER DEFAULT 0, on_plexpyupdate INTEGER DEFAULT 0, '
|
||||
'on_plexpydbcorrupt INTEGER DEFAULT 0, '
|
||||
'on_play_subject TEXT, on_stop_subject TEXT, on_pause_subject TEXT, '
|
||||
'on_resume_subject TEXT, on_change_subject TEXT, on_buffer_subject TEXT, on_watched_subject TEXT, '
|
||||
'on_created_subject TEXT, on_extdown_subject TEXT, on_intdown_subject TEXT, '
|
||||
'on_extup_subject TEXT, on_intup_subject TEXT, on_pmsupdate_subject TEXT, '
|
||||
'on_concurrent_subject TEXT, on_newdevice_subject TEXT, on_plexpyupdate_subject TEXT, '
|
||||
'on_plexpydbcorrupt_subject TEXT, '
|
||||
'on_play_body TEXT, on_stop_body TEXT, on_pause_body TEXT, '
|
||||
'on_resume_body TEXT, on_change_body TEXT, on_buffer_body TEXT, on_watched_body TEXT, '
|
||||
'on_created_body TEXT, on_extdown_body TEXT, on_intdown_body TEXT, '
|
||||
'on_extup_body TEXT, on_intup_body TEXT, on_pmsupdate_body TEXT, '
|
||||
'on_concurrent_body TEXT, on_newdevice_body TEXT, on_plexpyupdate_body TEXT, '
|
||||
'on_plexpydbcorrupt_body TEXT, '
|
||||
'custom_conditions TEXT, custom_conditions_logic TEXT)'
|
||||
)
|
||||
|
||||
@@ -1203,6 +1216,54 @@ def dbcheck():
|
||||
'ALTER TABLE sessions ADD COLUMN stream_video_full_resolution TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT video_dynamic_range FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN video_dynamic_range TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN stream_video_dynamic_range TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT channel_identifier FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN channel_call_sign TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN channel_identifier TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN channel_thumb TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT originally_available_at FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN originally_available_at TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN added_at INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT guid FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN guid TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT reference_id FROM session_history')
|
||||
@@ -1265,6 +1326,18 @@ def dbcheck():
|
||||
'ALTER TABLE session_history ADD COLUMN relayed INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade session_history table from earlier versions
|
||||
try:
|
||||
result = c_db.execute('SELECT platform FROM session_history '
|
||||
'WHERE platform = "windows"').fetchall()
|
||||
if len(result) > 0:
|
||||
logger.debug(u"Altering database. Capitalizing Windows platform values in session_history table.")
|
||||
c_db.execute(
|
||||
'UPDATE session_history SET platform = "Windows" WHERE platform = "windows" '
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
logger.warn(u"Unable to capitalize Windows platform values in session_history table.")
|
||||
|
||||
# Upgrade session_history_metadata table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT full_title FROM session_history_metadata')
|
||||
@@ -1310,6 +1383,24 @@ def dbcheck():
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN original_title TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history_metadata table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT live 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 live INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN channel_call_sign TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN channel_identifier TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_metadata ADD COLUMN channel_thumb TEXT'
|
||||
)
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT transcode_decision FROM session_history_media_info')
|
||||
@@ -1541,6 +1632,26 @@ def dbcheck():
|
||||
'ELSE stream_video_resolution || "p" END)'
|
||||
)
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT video_dynamic_range FROM session_history_media_info')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table session_history_media_info.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_media_info ADD COLUMN video_dynamic_range TEXT '
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_media_info ADD COLUMN stream_video_dynamic_range TEXT '
|
||||
)
|
||||
|
||||
result = c_db.execute('SELECT * FROM session_history_media_info '
|
||||
'WHERE video_dynamic_range = "SDR" AND stream_video_dynamic_range = "HDR"').fetchone()
|
||||
if result:
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET stream_video_dynamic_range = "SDR" '
|
||||
'WHERE video_dynamic_range = "SDR" AND stream_video_dynamic_range = "HDR"'
|
||||
)
|
||||
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT do_notify FROM users')
|
||||
@@ -1625,6 +1736,15 @@ def dbcheck():
|
||||
'ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0'
|
||||
)
|
||||
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT is_active FROM users')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table users.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1'
|
||||
)
|
||||
|
||||
# Upgrade notify_log table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT poster_url FROM notify_log')
|
||||
@@ -1788,6 +1908,24 @@ def dbcheck():
|
||||
'ALTER TABLE library_sections ADD COLUMN agent TEXT'
|
||||
)
|
||||
|
||||
# Upgrade library_sections table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT custom_art_url FROM library_sections')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table library_sections.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE library_sections ADD COLUMN custom_art_url TEXT'
|
||||
)
|
||||
|
||||
# Upgrade library_sections table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT is_active FROM library_sections')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table library_sections.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE library_sections ADD COLUMN is_active INTEGER DEFAULT 1'
|
||||
)
|
||||
|
||||
# Upgrade users table from earlier versions (remove UNIQUE constraint on username)
|
||||
try:
|
||||
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="users"').fetchone()
|
||||
@@ -1874,6 +2012,21 @@ def dbcheck():
|
||||
'ALTER TABLE notifiers ADD COLUMN on_change_body TEXT'
|
||||
)
|
||||
|
||||
# Upgrade notifiers table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT on_plexpydbcorrupt FROM notifiers')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table notifiers.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE notifiers ADD COLUMN on_plexpydbcorrupt INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE notifiers ADD COLUMN on_plexpydbcorrupt_subject TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE notifiers ADD COLUMN on_plexpydbcorrupt_body TEXT'
|
||||
)
|
||||
|
||||
# Upgrade tvmaze_lookup table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT rating_key FROM tvmaze_lookup')
|
||||
@@ -1974,7 +2127,7 @@ def upgrade():
|
||||
libraries.update_libraries_db_notify()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False, checkout=False):
|
||||
def shutdown(restart=False, update=False, checkout=False, reset=False):
|
||||
webstart.stop()
|
||||
|
||||
# Shutdown the websocket connection
|
||||
@@ -2009,6 +2162,13 @@ def shutdown(restart=False, update=False, checkout=False):
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli failed to switch git branch: %s. Restarting." % e)
|
||||
|
||||
if reset:
|
||||
logger.info(u"Tautulli is resetting the git install...")
|
||||
try:
|
||||
versioncheck.reset_git_install()
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli failed to reset git install: %s. Restarting." % e)
|
||||
|
||||
if CREATEPID:
|
||||
logger.info(u"Removing pidfile %s", PIDFILE)
|
||||
os.remove(PIDFILE)
|
||||
|
@@ -58,9 +58,19 @@ class ActivityHandler(object):
|
||||
|
||||
return None
|
||||
|
||||
def get_live_session(self):
|
||||
def get_metadata(self, skip_cache=False):
|
||||
cache_key = None if skip_cache else self.get_session_key()
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
session_list = pms_connect.get_current_activity()
|
||||
metadata = pms_connect.get_metadata_details(rating_key=self.get_rating_key(), cache_key=cache_key)
|
||||
|
||||
if metadata:
|
||||
return metadata
|
||||
|
||||
return None
|
||||
|
||||
def get_live_session(self, skip_cache=False):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
session_list = pms_connect.get_current_activity(skip_cache=skip_cache)
|
||||
|
||||
if session_list:
|
||||
for session in session_list['sessions']:
|
||||
@@ -94,7 +104,7 @@ class ActivityHandler(object):
|
||||
|
||||
def on_start(self):
|
||||
if self.is_valid_session():
|
||||
session = self.get_live_session()
|
||||
session = self.get_live_session(skip_cache=True)
|
||||
|
||||
if not session:
|
||||
return
|
||||
@@ -107,9 +117,9 @@ class ActivityHandler(object):
|
||||
if not session:
|
||||
return
|
||||
|
||||
logger.debug(u"Tautulli ActivityHandler :: Session %s started by user %s (%s) with ratingKey %s (%s)."
|
||||
logger.debug(u"Tautulli ActivityHandler :: Session %s started by user %s (%s) with ratingKey %s (%s)%s."
|
||||
% (str(session['session_key']), str(session['user_id']), session['username'],
|
||||
str(session['rating_key']), session['full_title']))
|
||||
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||
|
||||
@@ -269,11 +279,20 @@ class ActivityHandler(object):
|
||||
last_transcode_key = db_session['transcode_key'].split('/')[-1]
|
||||
last_paused = db_session['last_paused']
|
||||
last_rating_key_websocket = db_session['rating_key_websocket']
|
||||
last_guid = db_session['guid']
|
||||
|
||||
this_guid = last_guid
|
||||
# Check guid for live TV metadata every 60 seconds
|
||||
if db_session['live'] and int(time.time()) - db_session['stopped'] > 60:
|
||||
metadata = self.get_metadata(skip_cache=True)
|
||||
if metadata:
|
||||
this_guid = metadata['guid']
|
||||
|
||||
# Make sure the same item is being played
|
||||
if this_rating_key == last_rating_key \
|
||||
or this_rating_key == last_rating_key_websocket \
|
||||
or this_live_uuid == last_live_uuid:
|
||||
if (this_rating_key == last_rating_key
|
||||
or this_rating_key == last_rating_key_websocket
|
||||
or this_live_uuid == last_live_uuid) \
|
||||
and this_guid == last_guid:
|
||||
# Update the session state and viewOffset
|
||||
if this_state == 'playing':
|
||||
# Update the session in our temp session table
|
||||
|
@@ -61,6 +61,9 @@ class ActivityProcessor(object):
|
||||
'platform': session.get('platform', ''),
|
||||
'parent_rating_key': session.get('parent_rating_key', ''),
|
||||
'grandparent_rating_key': session.get('grandparent_rating_key', ''),
|
||||
'originally_available_at': session.get('originally_available_at', ''),
|
||||
'added_at': session.get('added_at', ''),
|
||||
'guid': session.get('guid', ''),
|
||||
'view_offset': session.get('view_offset', ''),
|
||||
'duration': session.get('duration', ''),
|
||||
'video_decision': session.get('video_decision', ''),
|
||||
@@ -78,6 +81,7 @@ class ActivityProcessor(object):
|
||||
'video_framerate': session.get('video_framerate', ''),
|
||||
'video_scan_type': session.get('video_scan_type', ''),
|
||||
'video_full_resolution': session.get('video_full_resolution', ''),
|
||||
'video_dynamic_range': session.get('video_dynamic_range', ''),
|
||||
'aspect_ratio': session.get('aspect_ratio', ''),
|
||||
'audio_codec': session.get('audio_codec', ''),
|
||||
'audio_bitrate': session.get('audio_bitrate', ''),
|
||||
@@ -110,6 +114,7 @@ class ActivityProcessor(object):
|
||||
'stream_video_framerate': session.get('stream_video_framerate', ''),
|
||||
'stream_video_scan_type': session.get('stream_video_scan_type', ''),
|
||||
'stream_video_full_resolution': session.get('stream_video_full_resolution', ''),
|
||||
'stream_video_dynamic_range': session.get('stream_video_dynamic_range', ''),
|
||||
'stream_audio_decision': session.get('stream_audio_decision', ''),
|
||||
'stream_audio_codec': session.get('stream_audio_codec', ''),
|
||||
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
|
||||
@@ -123,6 +128,9 @@ class ActivityProcessor(object):
|
||||
'relayed': session.get('relayed', 0),
|
||||
'rating_key_websocket': session.get('rating_key_websocket', ''),
|
||||
'raw_stream_info': json.dumps(session),
|
||||
'channel_call_sign': session.get('channel_call_sign', ''),
|
||||
'channel_identifier': session.get('channel_identifier', ''),
|
||||
'channel_thumb': session.get('channel_thumb', ''),
|
||||
'stopped': int(time.time())
|
||||
}
|
||||
|
||||
@@ -141,6 +149,10 @@ class ActivityProcessor(object):
|
||||
timestamp = {'started': started}
|
||||
self.db.upsert('sessions', timestamp, keys)
|
||||
|
||||
# Add Live TV library if it hasn't been added
|
||||
if values['live']:
|
||||
libraries.add_live_tv_library()
|
||||
|
||||
return True
|
||||
|
||||
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
|
||||
@@ -233,7 +245,12 @@ class ActivityProcessor(object):
|
||||
if not is_import:
|
||||
logger.debug(u"Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||
if session['live']:
|
||||
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']),
|
||||
cache_key=session['session_key'],
|
||||
return_cache=True)
|
||||
else:
|
||||
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||
if not metadata:
|
||||
return False
|
||||
else:
|
||||
@@ -277,38 +294,57 @@ class ActivityProcessor(object):
|
||||
# % session['session_key'])
|
||||
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
|
||||
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history ' \
|
||||
'WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 '
|
||||
|
||||
args = [session['user_id'], session['rating_key']]
|
||||
|
||||
result = self.db.select(query=query, args=args)
|
||||
|
||||
new_session = prev_session = None
|
||||
prev_progress_percent = media_watched_percent = 0
|
||||
# Get the last insert row id
|
||||
last_id = self.db.last_insert_id()
|
||||
new_session = prev_session = None
|
||||
prev_progress_percent = media_watched_percent = 0
|
||||
|
||||
if len(result) > 1:
|
||||
new_session = {'id': result[0]['id'],
|
||||
'rating_key': result[0]['rating_key'],
|
||||
'view_offset': result[0]['view_offset'],
|
||||
'user_id': result[0]['user_id'],
|
||||
'reference_id': result[0]['reference_id']}
|
||||
if session['live']:
|
||||
# Check if we should group the session, select the last guid from the user
|
||||
query = 'SELECT session_history.id, session_history_metadata.guid, session_history.reference_id ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id == session_history_metadata.id ' \
|
||||
'WHERE session_history.user_id = ? ORDER BY session_history.id DESC LIMIT 1 '
|
||||
|
||||
prev_session = {'id': result[1]['id'],
|
||||
'rating_key': result[1]['rating_key'],
|
||||
'view_offset': result[1]['view_offset'],
|
||||
'user_id': result[1]['user_id'],
|
||||
'reference_id': result[1]['reference_id']}
|
||||
args = [session['user_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)
|
||||
result = self.db.select(query=query, args=args)
|
||||
|
||||
if len(result) > 0:
|
||||
new_session = {'id': last_id,
|
||||
'guid': metadata['guid'],
|
||||
'reference_id': last_id}
|
||||
|
||||
prev_session = {'id': result[0]['id'],
|
||||
'guid': result[0]['guid'],
|
||||
'reference_id': result[0]['reference_id']}
|
||||
|
||||
else:
|
||||
# Check if we should group the session, select the last two rows from the user
|
||||
query = 'SELECT id, rating_key, view_offset, reference_id FROM session_history ' \
|
||||
'WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 '
|
||||
|
||||
args = [session['user_id'], session['rating_key']]
|
||||
|
||||
result = self.db.select(query=query, args=args)
|
||||
|
||||
if len(result) > 1:
|
||||
new_session = {'id': result[0]['id'],
|
||||
'rating_key': result[0]['rating_key'],
|
||||
'view_offset': result[0]['view_offset'],
|
||||
'reference_id': result[0]['reference_id']}
|
||||
|
||||
prev_session = {'id': result[1]['id'],
|
||||
'rating_key': result[1]['rating_key'],
|
||||
'view_offset': result[1]['view_offset'],
|
||||
'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 = ? '
|
||||
|
||||
@@ -319,7 +355,8 @@ class ActivityProcessor(object):
|
||||
if prev_session is None and new_session is None:
|
||||
args = [last_id, last_id]
|
||||
elif prev_progress_percent < media_watched_percent and \
|
||||
prev_session['view_offset'] <= new_session['view_offset']:
|
||||
prev_session['view_offset'] <= new_session['view_offset'] or \
|
||||
session['live'] and prev_session['guid'] == new_session['guid']:
|
||||
args = [prev_session['reference_id'], new_session['id']]
|
||||
else:
|
||||
args = [new_session['id'], new_session['id']]
|
||||
@@ -353,6 +390,7 @@ class ActivityProcessor(object):
|
||||
'video_framerate': session['video_framerate'],
|
||||
'video_scan_type': session['video_scan_type'],
|
||||
'video_full_resolution': session['video_full_resolution'],
|
||||
'video_dynamic_range': session['video_dynamic_range'],
|
||||
'aspect_ratio': session['aspect_ratio'],
|
||||
'audio_codec': session['audio_codec'],
|
||||
'audio_bitrate': session['audio_bitrate'],
|
||||
@@ -387,6 +425,7 @@ class ActivityProcessor(object):
|
||||
'stream_video_framerate': session['stream_video_framerate'],
|
||||
'stream_video_scan_type': session['stream_video_scan_type'],
|
||||
'stream_video_full_resolution': session['stream_video_full_resolution'],
|
||||
'stream_video_dynamic_range': session['stream_video_dynamic_range'],
|
||||
'stream_audio_decision': session['stream_audio_decision'],
|
||||
'stream_audio_codec': session['stream_audio_codec'],
|
||||
'stream_audio_bitrate': session['stream_audio_bitrate'],
|
||||
@@ -449,7 +488,11 @@ class ActivityProcessor(object):
|
||||
'actors': actors,
|
||||
'genres': genres,
|
||||
'studio': metadata['studio'],
|
||||
'labels': labels
|
||||
'labels': labels,
|
||||
'live': session['live'],
|
||||
'channel_call_sign': media_info.get('channel_call_sign', ''),
|
||||
'channel_identifier': media_info.get('channel_identifier', ''),
|
||||
'channel_thumb': media_info.get('channel_thumb', '')
|
||||
}
|
||||
|
||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_metadata transaction..."
|
||||
|
@@ -116,11 +116,11 @@ class API2:
|
||||
# Allow override for the api.
|
||||
self._api_out_type = kwargs.pop('out_type', 'json')
|
||||
|
||||
if 'app' in kwargs and kwargs.pop('app') == 'true':
|
||||
if 'app' in kwargs and helpers.bool_true(kwargs.pop('app')):
|
||||
self._api_app = True
|
||||
|
||||
if plexpy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
if self._api_apikey == plexpy.CONFIG.API_KEY or (self._api_app and self._api_apikey == mobile_app.TEMP_DEVICE_TOKEN):
|
||||
if self._api_apikey == plexpy.CONFIG.API_KEY or (self._api_app and self._api_apikey == mobile_app.get_temp_device_token()):
|
||||
self._api_authenticated = True
|
||||
|
||||
elif self._api_app and mobile_app.get_mobile_device_by_token(self._api_apikey):
|
||||
@@ -292,7 +292,7 @@ class API2:
|
||||
def sql(self, query=''):
|
||||
""" Query the Tautulli database with raw SQL. Automatically makes a backup of
|
||||
the database if the latest backup is older then 24h. `api_sql` must be
|
||||
manually enabled in the config file.
|
||||
manually enabled in the config file while Tautulli is shut down.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
@@ -404,7 +404,7 @@ class API2:
|
||||
if result:
|
||||
self._api_msg = 'Device registration successful.'
|
||||
self._api_result_type = 'success'
|
||||
mobile_app.TEMP_DEVICE_TOKEN = None
|
||||
mobile_app.set_temp_device_token(None)
|
||||
else:
|
||||
self._api_msg = 'Device registartion failed: database error.'
|
||||
self._api_result_type = 'error'
|
||||
@@ -619,9 +619,9 @@ General optional parameters:
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||
try:
|
||||
if self._api_debug:
|
||||
out = json.dumps(out, indent=4, sort_keys=True, ensure_ascii=False).encode('utf-8')
|
||||
out = json.dumps(out, indent=4, sort_keys=True, ensure_ascii=False)
|
||||
else:
|
||||
out = json.dumps(out, ensure_ascii=False).encode('utf-8')
|
||||
out = json.dumps(out, ensure_ascii=False)
|
||||
if self._api_callback is not None:
|
||||
cherrypy.response.headers['Content-Type'] = 'application/javascript'
|
||||
# wrap with JSONP call if requested
|
||||
@@ -634,7 +634,7 @@ General optional parameters:
|
||||
out['result'] = 'error'
|
||||
|
||||
elif self._api_out_type == 'xml':
|
||||
cherrypy.response.headers['Content-Type'] = 'application/xml'
|
||||
cherrypy.response.headers['Content-Type'] = 'application/xml;charset=UTF-8'
|
||||
try:
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
except Exception as e:
|
||||
@@ -655,7 +655,7 @@ General optional parameters:
|
||||
</response>
|
||||
''' % e
|
||||
|
||||
return out
|
||||
return out.encode('utf-8')
|
||||
|
||||
def _api_run(self, *args, **kwargs):
|
||||
""" handles the stuff from the handler """
|
||||
|
@@ -34,11 +34,26 @@ DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
||||
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
||||
DEFAULT_ART = "interfaces/default/images/art.png"
|
||||
DEFAULT_LIVE_TV_POSTER_THUMB = "interfaces/default/images/poster-live.png"
|
||||
DEFAULT_LIVE_TV_ART = "interfaces/default/images/art-live.png"
|
||||
DEFAULT_LIVE_TV_ART_FULL = "interfaces/default/images/art-live-full.png"
|
||||
|
||||
ONLINE_POSTER_THUMB = "https://tautulli.com/images/poster.png"
|
||||
ONLINE_COVER_THUMB = "https://tautulli.com/images/cover.png"
|
||||
ONLINE_ART = "https://tautulli.com/images/art.png"
|
||||
|
||||
LIVE_TV_SECTION_ID = 999999 # Fake section_id for Live TV library
|
||||
LIVE_TV_SECTION_NAME = "Live TV" # Fake section_name for Live TV library
|
||||
|
||||
DEFAULT_IMAGES = {
|
||||
'poster': DEFAULT_POSTER_THUMB,
|
||||
'cover': DEFAULT_COVER_THUMB,
|
||||
'art': DEFAULT_ART,
|
||||
'poster-live': DEFAULT_LIVE_TV_POSTER_THUMB,
|
||||
'art-live': DEFAULT_LIVE_TV_ART,
|
||||
'art-live-full': DEFAULT_LIVE_TV_ART_FULL
|
||||
}
|
||||
|
||||
MEDIA_TYPE_HEADERS = {
|
||||
'movie': 'Movies',
|
||||
'show': 'TV Shows',
|
||||
@@ -54,7 +69,9 @@ PLATFORM_NAME_OVERRIDES = {
|
||||
'Mystery 3': 'Playstation 3',
|
||||
'Mystery 4': 'Playstation 4',
|
||||
'Mystery 5': 'Xbox 360',
|
||||
'WebMAF': 'Playstation 4'
|
||||
'WebMAF': 'Playstation 4',
|
||||
'windows': 'Windows',
|
||||
'osx': 'macOS'
|
||||
}
|
||||
|
||||
PMS_PLATFORM_NAME_OVERRIDES = {
|
||||
@@ -324,10 +341,16 @@ NOTIFICATION_PARAMETERS = [
|
||||
'category': 'Stream Details',
|
||||
'parameters': [
|
||||
{'name': 'Streams', 'type': 'int', 'value': 'streams', 'description': 'The number of concurrent streams.'},
|
||||
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the person streaming.'},
|
||||
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the person streaming.'},
|
||||
{'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the person streaming.'},
|
||||
{'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the person streaming.'},
|
||||
{'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays', 'description': 'The number of concurrent direct plays.'},
|
||||
{'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams', 'description': 'The number of concurrent direct streams.'},
|
||||
{'name': 'Transcodes', 'type': 'int', 'value': 'transcodes', 'description': 'The number of concurrent transcodes.'},
|
||||
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the user streaming.'},
|
||||
{'name': 'User Direct Plays', 'type': 'int', 'value': 'user_direct_plays', 'description': 'The number of concurrent direct plays by the user streaming.'},
|
||||
{'name': 'User Direct Streams', 'type': 'int', 'value': 'user_direct_streams', 'description': 'The number of concurrent direct streams by the user streaming.'},
|
||||
{'name': 'User Transcodes', 'type': 'int', 'value': 'user_transcodes', 'description': 'The number of concurrent transcodes by the user streaming.'},
|
||||
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the user streaming.'},
|
||||
{'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the user streaming.'},
|
||||
{'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the user streaming.'},
|
||||
{'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'},
|
||||
{'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'},
|
||||
{'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'},
|
||||
@@ -349,6 +372,9 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', 'description': 'The optimized version profile of the stream.'},
|
||||
{'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', 'description': 'If the stream is an synced version.', 'example': '0 or 1'},
|
||||
{'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.', 'example': '0 or 1'},
|
||||
{'name': 'Channel Call Sign', 'type': 'str', 'value': 'channel_call_sign', 'description': 'The Live TV channel call sign.'},
|
||||
{'name': 'Channel Identifier', 'type': 'str', 'value': 'channel_identifier', 'description': 'The Live TV channel number.'},
|
||||
{'name': 'Channel Thumb', 'type': 'str', 'value': 'channel_thumb', 'description': 'The URL for the Live TV channel logo.'},
|
||||
{'name': 'Secure', 'type': 'int', 'value': 'secure', 'description': 'If the stream is using a secure connection.', 'example': '0 or 1'},
|
||||
{'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.', 'example': '0 or 1'},
|
||||
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'},
|
||||
@@ -361,6 +387,12 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', 'description': 'The video codec level of the stream.'},
|
||||
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'},
|
||||
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'},
|
||||
{'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling', 'description': 'The video chroma subsampling of the stream.'},
|
||||
{'name': 'Stream Video Color Primaries', 'type': 'str', 'value': 'stream_video_color_primaries', 'description': 'The video color primaries of the stream.'},
|
||||
{'name': 'Stream Video Color Range', 'type': 'str', 'value': 'stream_video_color_range', 'description': 'The video color range of the stream.'},
|
||||
{'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space', 'description': 'The video color space of the stream.'},
|
||||
{'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc', 'description': 'The video transfer function of the stream.'},
|
||||
{'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range', 'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'},
|
||||
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'},
|
||||
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'},
|
||||
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'},
|
||||
@@ -468,6 +500,12 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', 'description': 'The video codec level of the original media.'},
|
||||
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'},
|
||||
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'},
|
||||
{'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling', 'description': 'The video chroma subsampling of the original media.'},
|
||||
{'name': 'Video Color Primaries', 'type': 'str', 'value': 'video_color_primaries', 'description': 'The video color primaries of the original media.'},
|
||||
{'name': 'Video Color Range', 'type': 'str', 'value': 'video_color_range', 'description': 'The video color range of the original media.'},
|
||||
{'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space', 'description': 'The video color space of the original media.'},
|
||||
{'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc', 'description': 'The video transfer function of the original media.'},
|
||||
{'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range', 'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'},
|
||||
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'},
|
||||
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'},
|
||||
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'},
|
||||
@@ -499,6 +537,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
|
||||
{'name': 'Parent Rating Key', 'type': 'int', 'value': 'parent_rating_key', 'description': 'The unique identifier for the season or album.'},
|
||||
{'name': 'Grandparent Rating Key', 'type': 'int', 'value': 'grandparent_rating_key', 'description': 'The unique identifier for the TV show or artist.'},
|
||||
{'name': 'Art', 'type': 'str', 'value': 'art', 'description': 'The Plex background art for the media.'},
|
||||
{'name': 'Thumb', 'type': 'str', 'value': 'thumb', 'description': 'The Plex thumbnail for the movie or episode.'},
|
||||
{'name': 'Parent Thumb', 'type': 'str', 'value': 'parent_thumb', 'description': 'The Plex thumbnail for the season or album.'},
|
||||
{'name': 'Grandparent Thumb', 'type': 'str', 'value': 'grandparent_thumb', 'description': 'The Plex thumbnail for the TV show or artist.'},
|
||||
|
@@ -68,6 +68,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
|
||||
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||
'ADD_LIVE_TV_LIBRARY': (int, 'Advanced', 1),
|
||||
'ANON_REDIRECT': (str, 'General', 'http://www.nullrefer.com/?'),
|
||||
'API_ENABLED': (int, 'General', 1),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
@@ -337,9 +338,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'NMA_ON_NEWDEVICE': (int, 'NMA', 0),
|
||||
'NOTIFICATION_THREADS': (int, 'Advanced', 2),
|
||||
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
|
||||
'NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||
'NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 1),
|
||||
'NOTIFY_GROUP_RECENTLY_ADDED_PARENT': (int, 'Monitoring', 1),
|
||||
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
|
||||
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
||||
@@ -603,6 +604,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'UPDATE_LABELS': (int, 'General', 1),
|
||||
'UPDATE_LIBRARIES_DB_NOTIFY': (int, 'General', 1),
|
||||
'UPDATE_NOTIFIERS_DB': (int, 'General', 1),
|
||||
'VERBOSE_LOGS': (int, 'Advanced', 1),
|
||||
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
||||
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||
'WEBSOCKET_MONITOR_PING_PONG': (int, 'Advanced', 0),
|
||||
@@ -932,3 +934,9 @@ class Config(object):
|
||||
self.GEOIP_DB = os.path.join(plexpy.DATA_DIR, 'GeoLite2-City.mmdb')
|
||||
|
||||
self.CONFIG_VERSION = 14
|
||||
|
||||
if self.CONFIG_VERSION == 14:
|
||||
if plexpy.DOCKER:
|
||||
self.PLEXPY_AUTO_UPDATE = 0
|
||||
|
||||
self.CONFIG_VERSION == 15
|
||||
|
@@ -21,6 +21,7 @@ import threading
|
||||
import time
|
||||
|
||||
import plexpy
|
||||
import helpers
|
||||
import logger
|
||||
|
||||
FILENAME = "tautulli.db"
|
||||
@@ -33,31 +34,82 @@ def integrity_check():
|
||||
return result
|
||||
|
||||
|
||||
def drop_session_db():
|
||||
monitor_db = MonitorDatabase()
|
||||
monitor_db.action('DROP TABLE sessions')
|
||||
def clear_table(table=None):
|
||||
if table:
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
|
||||
def clear_history_tables():
|
||||
logger.debug(u"Tautulli Database :: Deleting all session_history records... No turning back now bub.")
|
||||
monitor_db = MonitorDatabase()
|
||||
monitor_db.action('DELETE FROM session_history')
|
||||
monitor_db.action('DELETE FROM session_history_media_info')
|
||||
monitor_db.action('DELETE FROM session_history_metadata')
|
||||
monitor_db.action('VACUUM')
|
||||
logger.debug(u"Tautulli Database :: Clearing database table '%s'." % table)
|
||||
try:
|
||||
monitor_db.action('DELETE FROM %s' % table)
|
||||
monitor_db.action('VACUUM')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Database :: Failed to clear database table '%s': %s." % (table, e))
|
||||
return False
|
||||
|
||||
|
||||
def delete_sessions():
|
||||
logger.debug(u"Tautulli Database :: Clearing temporary sessions from database.")
|
||||
monitor_db = MonitorDatabase()
|
||||
logger.info(u"Tautulli Database :: Clearing temporary sessions from database.")
|
||||
return clear_table('sessions')
|
||||
|
||||
try:
|
||||
monitor_db.action('DELETE FROM sessions')
|
||||
monitor_db.action('VACUUM')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Database :: Unable to clear temporary sessions from database: %s." % e)
|
||||
return False
|
||||
|
||||
def delete_recently_added():
|
||||
logger.info(u"Tautulli Database :: Clearing recently added items from database.")
|
||||
return clear_table('recently_added')
|
||||
|
||||
|
||||
def delete_rows_from_table(table, row_ids):
|
||||
if row_ids and isinstance(row_ids, basestring):
|
||||
row_ids = map(helpers.cast_to_int, row_ids.split(','))
|
||||
|
||||
if row_ids:
|
||||
logger.info(u"Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
|
||||
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids))
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
try:
|
||||
monitor_db.action(query, row_ids)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, row_ids))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def delete_session_history_rows(row_ids=None):
|
||||
success = []
|
||||
for table in ('session_history', 'session_history_media_info', 'session_history_metadata'):
|
||||
success.append(delete_rows_from_table(table=table, row_ids=row_ids))
|
||||
return all(success)
|
||||
|
||||
|
||||
def delete_user_history(user_id=None):
|
||||
if str(user_id).isdigit():
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
# Get all history associated with the user_id
|
||||
result = monitor_db.select('SELECT id FROM session_history WHERE user_id = ?',
|
||||
[user_id])
|
||||
row_ids = [row['id'] for row in result]
|
||||
|
||||
logger.info(u"Tautulli Database :: Deleting all history for user_id %s from database." % user_id)
|
||||
return delete_session_history_rows(row_ids=row_ids)
|
||||
|
||||
|
||||
def delete_library_history(section_id=None):
|
||||
if str(section_id).isdigit():
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
# Get all history associated with the section_id
|
||||
result = monitor_db.select('SELECT session_history.id FROM session_history '
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id '
|
||||
'WHERE session_history_metadata.section_id = ?',
|
||||
[section_id])
|
||||
row_ids = [row['id'] for row in result]
|
||||
|
||||
logger.info(u"Tautulli Database :: Deleting all history for library section_id %s from database." % section_id)
|
||||
return delete_session_history_rows(row_ids=row_ids)
|
||||
|
||||
|
||||
def db_filename(filename=FILENAME):
|
||||
@@ -75,6 +127,7 @@ def make_backup(cleanup=False, scheduler=False):
|
||||
corrupt = ''
|
||||
if not integrity:
|
||||
corrupt = '.corrupt'
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpydbcorrupt'})
|
||||
|
||||
if scheduler:
|
||||
backup_file = 'tautulli.backup-{}{}.sched.db'.format(arrow.now().format('YYYYMMDDHHmmss'), corrupt)
|
||||
|
@@ -64,7 +64,7 @@ class DataFactory(object):
|
||||
|
||||
columns = [
|
||||
'session_history.reference_id',
|
||||
'session_history.id',
|
||||
'session_history.id AS row_id',
|
||||
'MAX(started) AS date',
|
||||
'MIN(started) AS started',
|
||||
'MAX(stopped) AS stopped',
|
||||
@@ -94,6 +94,10 @@ class DataFactory(object):
|
||||
'session_history_metadata.thumb',
|
||||
'session_history_metadata.parent_thumb',
|
||||
'session_history_metadata.grandparent_thumb',
|
||||
'session_history_metadata.live',
|
||||
'session_history_metadata.added_at',
|
||||
'session_history_metadata.originally_available_at',
|
||||
'session_history_metadata.guid',
|
||||
'MAX((CASE WHEN (view_offset IS NULL OR view_offset = "") THEN 0.1 ELSE view_offset * 1.0 END) / \
|
||||
(CASE WHEN (session_history_metadata.duration IS NULL OR session_history_metadata.duration = "") \
|
||||
THEN 1.0 ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete',
|
||||
@@ -112,7 +116,7 @@ class DataFactory(object):
|
||||
|
||||
columns_union = [
|
||||
'NULL AS reference_id',
|
||||
'NULL AS id',
|
||||
'NULL AS row_id',
|
||||
'started AS date',
|
||||
'started',
|
||||
'stopped',
|
||||
@@ -142,6 +146,10 @@ class DataFactory(object):
|
||||
'thumb',
|
||||
'parent_thumb',
|
||||
'grandparent_thumb',
|
||||
'live',
|
||||
'added_at',
|
||||
'originally_available_at',
|
||||
'guid',
|
||||
'MAX((CASE WHEN (view_offset IS NULL OR view_offset = "") THEN 0.1 ELSE view_offset * 1.0 END) / \
|
||||
(CASE WHEN (duration IS NULL OR duration = "") \
|
||||
THEN 1.0 ELSE duration * 1.0 END) * 100) AS percent_complete',
|
||||
@@ -206,6 +214,9 @@ class DataFactory(object):
|
||||
else:
|
||||
thumb = item['thumb']
|
||||
|
||||
if item['live']:
|
||||
item['percent_complete'] = 100
|
||||
|
||||
if item['percent_complete'] >= watched_percent[item['media_type']]:
|
||||
watched_status = 1
|
||||
elif item['percent_complete'] >= watched_percent[item['media_type']]/2:
|
||||
@@ -217,7 +228,7 @@ class DataFactory(object):
|
||||
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
||||
|
||||
row = {'reference_id': item['reference_id'],
|
||||
'id': item['id'],
|
||||
'row_id': item['row_id'],
|
||||
'date': item['date'],
|
||||
'started': item['started'],
|
||||
'stopped': item['stopped'],
|
||||
@@ -230,6 +241,7 @@ class DataFactory(object):
|
||||
'product': item['product'],
|
||||
'player': item['player'],
|
||||
'ip_address': item['ip_address'],
|
||||
'live': item['live'],
|
||||
'media_type': item['media_type'],
|
||||
'rating_key': item['rating_key'],
|
||||
'parent_rating_key': item['parent_rating_key'],
|
||||
@@ -243,6 +255,8 @@ class DataFactory(object):
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'thumb': thumb,
|
||||
'originally_available_at': item['originally_available_at'],
|
||||
'guid': item['guid'],
|
||||
'transcode_decision': item['transcode_decision'],
|
||||
'percent_complete': int(round(item['percent_complete'])),
|
||||
'watched_status': watched_status,
|
||||
@@ -286,7 +300,7 @@ class DataFactory(object):
|
||||
top_movies = []
|
||||
try:
|
||||
query = 'SELECT t.id, t.full_title, t.rating_key, t.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, t.live, t.guid, ' \
|
||||
'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) - ' \
|
||||
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
|
||||
@@ -322,7 +336,8 @@ class DataFactory(object):
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform': '',
|
||||
'platform': '',
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'row_id': item['id']
|
||||
}
|
||||
top_movies.append(row)
|
||||
@@ -336,7 +351,7 @@ class DataFactory(object):
|
||||
popular_movies = []
|
||||
try:
|
||||
query = 'SELECT t.id, t.full_title, t.rating_key, t.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, t.live, t.guid, ' \
|
||||
'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 ' \
|
||||
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
|
||||
@@ -372,6 +387,8 @@ class DataFactory(object):
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform': '',
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'row_id': item['id']
|
||||
}
|
||||
popular_movies.append(row)
|
||||
@@ -384,7 +401,7 @@ class DataFactory(object):
|
||||
top_tv = []
|
||||
try:
|
||||
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
|
||||
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
|
||||
't.rating_key, t.art, t.media_type, t.content_rating, t.labels, t.started, t.live, t.guid, ' \
|
||||
'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) - ' \
|
||||
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
|
||||
@@ -408,7 +425,7 @@ class DataFactory(object):
|
||||
'total_plays': item['total_plays'],
|
||||
'total_duration': item['total_duration'],
|
||||
'users_watched': '',
|
||||
'rating_key': item['grandparent_rating_key'],
|
||||
'rating_key': item['rating_key'] if item['live'] else item['grandparent_rating_key'],
|
||||
'last_play': item['last_watch'],
|
||||
'grandparent_thumb': item['grandparent_thumb'],
|
||||
'thumb': item['grandparent_thumb'],
|
||||
@@ -420,6 +437,8 @@ class DataFactory(object):
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform': '',
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'row_id': item['id']
|
||||
}
|
||||
top_tv.append(row)
|
||||
@@ -433,7 +452,7 @@ class DataFactory(object):
|
||||
popular_tv = []
|
||||
try:
|
||||
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
|
||||
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
|
||||
't.rating_key, t.art, t.media_type, t.content_rating, t.labels, t.started, t.live, t.guid, ' \
|
||||
'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 ' \
|
||||
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
|
||||
@@ -456,7 +475,7 @@ class DataFactory(object):
|
||||
for item in result:
|
||||
row = {'title': item['grandparent_title'],
|
||||
'users_watched': item['users_watched'],
|
||||
'rating_key': item['grandparent_rating_key'],
|
||||
'rating_key': item['rating_key'] if item['live'] else item['grandparent_rating_key'],
|
||||
'last_play': item['last_watch'],
|
||||
'total_plays': item['total_plays'],
|
||||
'grandparent_thumb': item['grandparent_thumb'],
|
||||
@@ -469,6 +488,8 @@ class DataFactory(object):
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform': '',
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'row_id': item['id']
|
||||
}
|
||||
popular_tv.append(row)
|
||||
@@ -482,7 +503,7 @@ class DataFactory(object):
|
||||
try:
|
||||
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, t.live, t.guid, ' \
|
||||
'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) - ' \
|
||||
' (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) ' \
|
||||
@@ -518,6 +539,8 @@ class DataFactory(object):
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform': '',
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'row_id': item['id']
|
||||
}
|
||||
top_music.append(row)
|
||||
@@ -532,7 +555,7 @@ class DataFactory(object):
|
||||
try:
|
||||
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, t.live, t.guid, ' \
|
||||
'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 ' \
|
||||
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
|
||||
@@ -568,6 +591,8 @@ class DataFactory(object):
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform': '',
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'row_id': item['id']
|
||||
}
|
||||
popular_music.append(row)
|
||||
@@ -684,7 +709,7 @@ class DataFactory(object):
|
||||
try:
|
||||
query = 'SELECT t.id, t.full_title, t.rating_key, t.thumb, t.grandparent_thumb, ' \
|
||||
't.user, t.user_id, t.custom_avatar_url as user_thumb, t.player, t.section_id, ' \
|
||||
't.art, t.media_type, t.content_rating, t.labels, ' \
|
||||
't.art, t.media_type, t.content_rating, t.labels, t.live, t.guid, ' \
|
||||
'(CASE WHEN t.friendly_name IS NULL THEN t.username ELSE t.friendly_name END) ' \
|
||||
' AS friendly_name, ' \
|
||||
'MAX(t.started) AS last_watch, ' \
|
||||
@@ -730,6 +755,8 @@ class DataFactory(object):
|
||||
'content_rating': item['content_rating'],
|
||||
'labels': item['labels'].split(';') if item['labels'] else (),
|
||||
'last_watch': item['last_watch'],
|
||||
'live': item['live'],
|
||||
'guid': item['guid'],
|
||||
'player': item['player']
|
||||
}
|
||||
last_watched.append(row)
|
||||
@@ -834,7 +861,8 @@ class DataFactory(object):
|
||||
|
||||
try:
|
||||
query = 'SELECT section_id, section_name, section_type, thumb AS library_thumb, ' \
|
||||
'custom_thumb_url AS custom_thumb, art, count, parent_count, child_count ' \
|
||||
'custom_thumb_url AS custom_thumb, art AS library_art, custom_art_url AS custom_art, ' \
|
||||
'count, parent_count, child_count ' \
|
||||
'FROM library_sections ' \
|
||||
'WHERE section_id IN (%s) ' \
|
||||
'ORDER BY section_type, count DESC, parent_count DESC, child_count DESC ' % ','.join(library_cards)
|
||||
@@ -851,11 +879,16 @@ class DataFactory(object):
|
||||
else:
|
||||
library_thumb = common.DEFAULT_COVER_THUMB
|
||||
|
||||
if item['custom_art'] and item['custom_art'] != item['library_art']:
|
||||
library_art = item['custom_art']
|
||||
else:
|
||||
library_art = item['library_art']
|
||||
|
||||
library = {'section_id': item['section_id'],
|
||||
'section_name': item['section_name'],
|
||||
'section_type': item['section_type'],
|
||||
'thumb': library_thumb,
|
||||
'art': item['art'],
|
||||
'art': library_art,
|
||||
'count': item['count'],
|
||||
'child_count': item['parent_count'],
|
||||
'grandchild_count': item['child_count']
|
||||
@@ -878,11 +911,12 @@ class DataFactory(object):
|
||||
query = 'SELECT bitrate, video_full_resolution, ' \
|
||||
'optimized_version, optimized_version_profile, optimized_version_title, ' \
|
||||
'synced_version, synced_version_profile, ' \
|
||||
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
|
||||
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, ' \
|
||||
'video_dynamic_range, aspect_ratio, ' \
|
||||
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
|
||||
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
|
||||
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
|
||||
'stream_video_framerate, ' \
|
||||
'stream_video_framerate, stream_video_dynamic_range, ' \
|
||||
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
||||
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
||||
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
||||
@@ -899,11 +933,12 @@ class DataFactory(object):
|
||||
query = 'SELECT bitrate, video_full_resolution, ' \
|
||||
'optimized_version, optimized_version_profile, optimized_version_title, ' \
|
||||
'synced_version, synced_version_profile, ' \
|
||||
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
|
||||
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, ' \
|
||||
'video_dynamic_range, aspect_ratio, ' \
|
||||
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
|
||||
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
|
||||
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
|
||||
'stream_video_framerate, ' \
|
||||
'stream_video_framerate, stream_video_dynamic_range, ' \
|
||||
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
||||
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
||||
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
||||
@@ -950,6 +985,7 @@ class DataFactory(object):
|
||||
'video_width': item['video_width'],
|
||||
'video_height': item['video_height'],
|
||||
'video_framerate': item['video_framerate'],
|
||||
'video_dynamic_range': item['video_dynamic_range'],
|
||||
'aspect_ratio': item['aspect_ratio'],
|
||||
'audio_codec': item['audio_codec'],
|
||||
'audio_bitrate': item['audio_bitrate'],
|
||||
@@ -966,6 +1002,7 @@ class DataFactory(object):
|
||||
'stream_video_width': item['stream_video_width'],
|
||||
'stream_video_height': item['stream_video_height'],
|
||||
'stream_video_framerate': item['stream_video_framerate'],
|
||||
'stream_video_dynamic_range': item['stream_video_dynamic_range'],
|
||||
'stream_audio_decision': item['stream_audio_decision'],
|
||||
'stream_audio_codec': item['stream_audio_codec'],
|
||||
'stream_audio_bitrate': item['stream_audio_bitrate'],
|
||||
@@ -988,10 +1025,17 @@ class DataFactory(object):
|
||||
stream_output = {k: v or '' for k, v in stream_output.iteritems()}
|
||||
return stream_output
|
||||
|
||||
def get_metadata_details(self, rating_key):
|
||||
def get_metadata_details(self, rating_key='', guid=''):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if rating_key:
|
||||
if rating_key or guid:
|
||||
if guid:
|
||||
where = 'session_history_metadata.guid LIKE ?'
|
||||
args = [guid.split('?')[0] + '%'] # SQLite LIKE wildcard
|
||||
else:
|
||||
where = 'session_history_metadata.rating_key = ?'
|
||||
args = [rating_key]
|
||||
|
||||
query = 'SELECT session_history_metadata.id, ' \
|
||||
'session_history_metadata.rating_key, session_history_metadata.parent_rating_key, ' \
|
||||
'session_history_metadata.grandparent_rating_key, session_history_metadata.title, ' \
|
||||
@@ -1011,15 +1055,18 @@ class DataFactory(object):
|
||||
'session_history_metadata.labels, ' \
|
||||
'session_history_media_info.container, session_history_media_info.bitrate, ' \
|
||||
'session_history_media_info.video_codec, session_history_media_info.video_resolution, ' \
|
||||
'session_history_media_info.video_full_resolution, ' \
|
||||
'session_history_media_info.video_framerate, session_history_media_info.audio_codec, ' \
|
||||
'session_history_media_info.audio_channels ' \
|
||||
'session_history_media_info.audio_channels, session_history_metadata.live, ' \
|
||||
'session_history_metadata.channel_call_sign, session_history_metadata.channel_identifier, ' \
|
||||
'session_history_metadata.channel_thumb ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN library_sections ON session_history_metadata.section_id = library_sections.section_id ' \
|
||||
'JOIN session_history_media_info ON session_history_metadata.id = session_history_media_info.id ' \
|
||||
'WHERE session_history_metadata.rating_key = ? ' \
|
||||
'WHERE %s ' \
|
||||
'ORDER BY session_history_metadata.id DESC ' \
|
||||
'LIMIT 1'
|
||||
result = monitor_db.select(query=query, args=[rating_key])
|
||||
'LIMIT 1' % where
|
||||
result = monitor_db.select(query=query, args=args)
|
||||
else:
|
||||
result = []
|
||||
|
||||
@@ -1036,9 +1083,13 @@ class DataFactory(object):
|
||||
'bitrate': item['bitrate'],
|
||||
'video_codec': item['video_codec'],
|
||||
'video_resolution': item['video_resolution'],
|
||||
'video_full_resolution': item['video_full_resolution'],
|
||||
'video_framerate': item['video_framerate'],
|
||||
'audio_codec': item['audio_codec'],
|
||||
'audio_channels': item['audio_channels']
|
||||
'audio_channels': item['audio_channels'],
|
||||
'channel_call_sign': item['channel_call_sign'],
|
||||
'channel_identifier': item['channel_identifier'],
|
||||
'channel_thumb': item['channel_thumb']
|
||||
}]
|
||||
|
||||
metadata = {'media_type': item['media_type'],
|
||||
@@ -1052,6 +1103,7 @@ class DataFactory(object):
|
||||
'media_index': item['media_index'],
|
||||
'studio': item['studio'],
|
||||
'title': item['title'],
|
||||
'full_title': item['full_title'],
|
||||
'content_rating': item['content_rating'],
|
||||
'summary': item['summary'],
|
||||
'tagline': item['tagline'],
|
||||
@@ -1074,6 +1126,7 @@ class DataFactory(object):
|
||||
'labels': labels,
|
||||
'library_name': item['section_name'],
|
||||
'section_id': item['section_id'],
|
||||
'live': item['live'],
|
||||
'media_info': media_info
|
||||
}
|
||||
metadata_list.append(metadata)
|
||||
@@ -1352,16 +1405,29 @@ class DataFactory(object):
|
||||
|
||||
return lookup_info
|
||||
|
||||
def delete_lookup_info(self, rating_key='', title=''):
|
||||
def delete_lookup_info(self, rating_key='', service='', delete_all=False):
|
||||
if not rating_key and not delete_all:
|
||||
logger.error(u"Tautulli DataFactory :: Unable to delete lookup info: rating_key not provided.")
|
||||
return False
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if rating_key:
|
||||
logger.info(u"Tautulli DataFactory :: Deleting lookup info for '%s' (rating_key %s) from the database."
|
||||
% (title, rating_key))
|
||||
result_tvmaze = monitor_db.action('DELETE FROM tvmaze_lookup WHERE rating_key = ?', [rating_key])
|
||||
logger.info(u"Tautulli DataFactory :: Deleting lookup info for rating_key %s from the database."
|
||||
% rating_key)
|
||||
result_themoviedb = monitor_db.action('DELETE FROM themoviedb_lookup WHERE rating_key = ?', [rating_key])
|
||||
result_tvmaze = monitor_db.action('DELETE FROM tvmaze_lookup WHERE rating_key = ?', [rating_key])
|
||||
result_musicbrainz = monitor_db.action('DELETE FROM musicbrainz_lookup WHERE rating_key = ?', [rating_key])
|
||||
return True if (result_tvmaze or result_themoviedb or result_musicbrainz) else False
|
||||
return bool(result_themoviedb or result_tvmaze or result_musicbrainz)
|
||||
elif service and delete_all:
|
||||
if service.lower() in ('themoviedb', 'tvmaze', 'musicbrainz'):
|
||||
logger.info(u"Tautulli DataFactory :: Deleting all lookup info for '%s' from the database."
|
||||
% service)
|
||||
result = monitor_db.action('DELETE FROM %s_lookup' % service.lower())
|
||||
return bool(result)
|
||||
else:
|
||||
logger.error(u"Tautulli DataFactory :: Unable to delete lookup info: invalid service '%s' provided."
|
||||
% service)
|
||||
|
||||
def get_search_query(self, rating_key=''):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
@@ -1495,22 +1561,6 @@ class DataFactory(object):
|
||||
|
||||
return key_list
|
||||
|
||||
def delete_session_history_rows(self, row_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if row_id.isdigit():
|
||||
logger.info(u"Tautulli DataFactory :: Deleting row id %s from the session history database." % row_id)
|
||||
session_history_del = \
|
||||
monitor_db.action('DELETE FROM session_history WHERE id = ?', [row_id])
|
||||
session_history_media_info_del = \
|
||||
monitor_db.action('DELETE FROM session_history_media_info WHERE id = ?', [row_id])
|
||||
session_history_metadata_del = \
|
||||
monitor_db.action('DELETE FROM session_history_metadata WHERE id = ?', [row_id])
|
||||
|
||||
return 'Deleted rows %s.' % row_id
|
||||
else:
|
||||
return 'Unable to delete rows. Input row not valid.'
|
||||
|
||||
def update_metadata(self, old_key_list='', new_key_list='', media_type=''):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
@@ -142,6 +142,9 @@ class DataTables(object):
|
||||
for w_ in w[1]:
|
||||
if w_ == None:
|
||||
c_where += w[0] + ' IS NULL OR '
|
||||
elif str(w_).startswith('LIKE '):
|
||||
c_where += w[0] + ' LIKE ? OR '
|
||||
args.append(w_[5:])
|
||||
else:
|
||||
c_where += w[0] + ' = ? OR '
|
||||
args.append(w_)
|
||||
@@ -149,6 +152,9 @@ class DataTables(object):
|
||||
else:
|
||||
if w[1] == None:
|
||||
c_where += w[0] + ' IS NULL AND '
|
||||
elif str(w[1]).startswith('LIKE '):
|
||||
c_where += w[0] + ' LIKE ? AND '
|
||||
args.append(w[1][5:])
|
||||
else:
|
||||
c_where += w[0] + ' = ? AND '
|
||||
args.append(w[1])
|
||||
|
277
plexpy/graphs.py
@@ -18,6 +18,7 @@ import datetime
|
||||
import plexpy
|
||||
import common
|
||||
import database
|
||||
import libraries
|
||||
import logger
|
||||
import session
|
||||
|
||||
@@ -42,15 +43,17 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
|
||||
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(live) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
'GROUP BY date(started, "unixepoch", "localtime"), %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
@@ -60,13 +63,17 @@ class Graphs(object):
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||
'FROM session_history ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN live = 1 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
'GROUP BY date_played ' \
|
||||
'ORDER BY started ASC' % (time_range, user_cond)
|
||||
@@ -85,6 +92,7 @@ class Graphs(object):
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
for date_item in sorted(date_list):
|
||||
date_string = date_item.strftime('%Y-%m-%d')
|
||||
@@ -92,20 +100,24 @@ class Graphs(object):
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
for item in result:
|
||||
if date_string == item['date_played']:
|
||||
series_1_value = item['tv_count']
|
||||
series_2_value = item['movie_count']
|
||||
series_3_value = item['music_count']
|
||||
series_4_value = item['live_count']
|
||||
break
|
||||
else:
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
|
||||
series_1.append(series_1_value)
|
||||
series_2.append(series_2_value)
|
||||
series_3.append(series_3_value)
|
||||
series_4.append(series_4_value)
|
||||
|
||||
series_1_output = {'name': 'TV',
|
||||
'data': series_1}
|
||||
@@ -113,9 +125,21 @@ class Graphs(object):
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Music',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Live TV',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_2_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_3_output)
|
||||
if libraries.has_library_type('live'):
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_per_dayofweek(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||
@@ -133,7 +157,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
@@ -146,10 +170,12 @@ class Graphs(object):
|
||||
'WHEN 4 THEN "Thursday" ' \
|
||||
'WHEN 5 THEN "Friday" ' \
|
||||
'ELSE "Saturday" END) AS dayofweek, ' \
|
||||
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(live) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
'GROUP BY strftime("%%w", datetime(started, "unixepoch", "localtime")), %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
@@ -167,13 +193,17 @@ class Graphs(object):
|
||||
'WHEN 4 THEN "Thursday" ' \
|
||||
'WHEN 5 THEN "Friday" ' \
|
||||
'ELSE "Saturday" END) AS dayofweek, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||
'FROM session_history ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN live = 1 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
'GROUP BY dayofweek ' \
|
||||
'ORDER BY daynumber' % (time_range, user_cond)
|
||||
@@ -194,26 +224,31 @@ class Graphs(object):
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
for day_item in days_list:
|
||||
categories.append(day_item)
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
for item in result:
|
||||
if day_item == item['dayofweek']:
|
||||
series_1_value = item['tv_count']
|
||||
series_2_value = item['movie_count']
|
||||
series_3_value = item['music_count']
|
||||
series_4_value = item['live_count']
|
||||
break
|
||||
else:
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
|
||||
series_1.append(series_1_value)
|
||||
series_2.append(series_2_value)
|
||||
series_3.append(series_3_value)
|
||||
series_4.append(series_4_value)
|
||||
|
||||
series_1_output = {'name': 'TV',
|
||||
'data': series_1}
|
||||
@@ -221,9 +256,21 @@ class Graphs(object):
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Music',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Live TV',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_2_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_3_output)
|
||||
if libraries.has_library_type('live'):
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_per_hourofday(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||
@@ -241,15 +288,17 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT strftime("%%H", datetime(started, "unixepoch", "localtime")) AS hourofday, ' \
|
||||
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(live) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
'GROUP BY strftime("%%H", datetime(started, "unixepoch", "localtime")) , %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
@@ -259,13 +308,17 @@ class Graphs(object):
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT strftime("%%H", datetime(started, "unixepoch", "localtime")) AS hourofday, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||
'FROM session_history ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN live = 1 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
'GROUP BY hourofday ' \
|
||||
'ORDER BY hourofday' % (time_range, user_cond)
|
||||
@@ -275,35 +328,40 @@ class Graphs(object):
|
||||
logger.warn(u"Tautulli Graphs :: Unable to execute database query for get_total_plays_per_hourofday: %s." % e)
|
||||
return None
|
||||
|
||||
hours_list = ['00','01','02','03','04','05',
|
||||
'06','07','08','09','10','11',
|
||||
'12','13','14','15','16','17',
|
||||
'18','19','20','21','22','23']
|
||||
hours_list = ['00', '01', '02', '03', '04', '05',
|
||||
'06', '07', '08', '09', '10', '11',
|
||||
'12', '13', '14', '15', '16', '17',
|
||||
'18', '19', '20', '21', '22', '23']
|
||||
|
||||
categories = []
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
for hour_item in hours_list:
|
||||
categories.append(hour_item)
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
for item in result:
|
||||
if hour_item == item['hourofday']:
|
||||
series_1_value = item['tv_count']
|
||||
series_2_value = item['movie_count']
|
||||
series_3_value = item['music_count']
|
||||
series_4_value = item['live_count']
|
||||
break
|
||||
else:
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
|
||||
series_1.append(series_1_value)
|
||||
series_2.append(series_2_value)
|
||||
series_3.append(series_3_value)
|
||||
series_4.append(series_4_value)
|
||||
|
||||
series_1_output = {'name': 'TV',
|
||||
'data': series_1}
|
||||
@@ -311,14 +369,24 @@ class Graphs(object):
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Music',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Live TV',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_2_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_3_output)
|
||||
if libraries.has_library_type('live'):
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, grouping=None):
|
||||
import time as time
|
||||
|
||||
if not time_range.isdigit():
|
||||
time_range = '12'
|
||||
|
||||
@@ -333,15 +401,17 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) AS datestring, ' \
|
||||
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(live) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")), %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
||||
@@ -351,13 +421,17 @@ class Graphs(object):
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) AS datestring, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||
'FROM session_history ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN live = 1 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS live_count ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
||||
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
|
||||
'ORDER BY datestring DESC LIMIT %s' % (time_range, user_cond, time_range)
|
||||
@@ -383,6 +457,7 @@ class Graphs(object):
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
for dt in sorted(month_range):
|
||||
date_string = dt.strftime('%Y-%m')
|
||||
@@ -390,20 +465,24 @@ class Graphs(object):
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
for item in result:
|
||||
if date_string == item['datestring']:
|
||||
series_1_value = item['tv_count']
|
||||
series_2_value = item['movie_count']
|
||||
series_3_value = item['music_count']
|
||||
series_4_value = item['live_count']
|
||||
break
|
||||
else:
|
||||
series_1_value = 0
|
||||
series_2_value = 0
|
||||
series_3_value = 0
|
||||
series_4_value = 0
|
||||
|
||||
series_1.append(series_1_value)
|
||||
series_2.append(series_2_value)
|
||||
series_3.append(series_3_value)
|
||||
series_4.append(series_4_value)
|
||||
|
||||
series_1_output = {'name': 'TV',
|
||||
'data': series_1}
|
||||
@@ -411,9 +490,21 @@ class Graphs(object):
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Music',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Live TV',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_2_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_3_output)
|
||||
if libraries.has_library_type('live'):
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||
@@ -431,16 +522,20 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
query = 'SELECT platform, ' \
|
||||
'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 = "track" THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(live) AS live_count, ' \
|
||||
'COUNT(id) AS total_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
'GROUP BY %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||
'GROUP BY platform ' \
|
||||
'ORDER BY total_count DESC ' \
|
||||
@@ -449,15 +544,19 @@ class Graphs(object):
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT platform, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN live = 1 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS live_count, ' \
|
||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||
'FROM session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||
'GROUP BY platform ' \
|
||||
'ORDER BY total_duration DESC ' \
|
||||
@@ -472,12 +571,14 @@ class Graphs(object):
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
for item in result:
|
||||
categories.append(common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']))
|
||||
series_1.append(item['tv_count'])
|
||||
series_2.append(item['movie_count'])
|
||||
series_3.append(item['music_count'])
|
||||
series_4.append(item['live_count'])
|
||||
|
||||
series_1_output = {'name': 'TV',
|
||||
'data': series_1}
|
||||
@@ -485,9 +586,21 @@ class Graphs(object):
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Music',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Live TV',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_2_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_3_output)
|
||||
if libraries.has_library_type('live'):
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||
@@ -505,7 +618,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
@@ -513,11 +626,15 @@ class Graphs(object):
|
||||
'users.user_id, users.username, ' \
|
||||
'(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" ' \
|
||||
' THEN users.username ELSE users.friendly_name END) AS friendly_name,' \
|
||||
'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 = "track" THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 THEN 1 ELSE 0 END) AS music_count, ' \
|
||||
'SUM(live) AS live_count, ' \
|
||||
'COUNT(session_history.id) AS total_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
'GROUP BY %s) ' \
|
||||
'AS session_history ' \
|
||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||
'GROUP BY session_history.user_id ' \
|
||||
@@ -530,15 +647,19 @@ class Graphs(object):
|
||||
'users.user_id, users.username, ' \
|
||||
'(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" ' \
|
||||
' THEN users.username ELSE users.friendly_name END) AS friendly_name,' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "episode" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "movie" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||
'SUM(CASE WHEN media_type = "track" AND live = 0 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count, ' \
|
||||
'SUM(CASE WHEN live = 1 AND stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS live_count, ' \
|
||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
|
||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||
'FROM session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id) ' \
|
||||
'AS session_history ' \
|
||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||
'GROUP BY session_history.user_id ' \
|
||||
@@ -554,6 +675,7 @@ class Graphs(object):
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
session_user_id = session.get_session_user_id()
|
||||
|
||||
@@ -565,6 +687,7 @@ class Graphs(object):
|
||||
series_1.append(item['tv_count'])
|
||||
series_2.append(item['movie_count'])
|
||||
series_3.append(item['music_count'])
|
||||
series_4.append(item['live_count'])
|
||||
|
||||
series_1_output = {'name': 'TV',
|
||||
'data': series_1}
|
||||
@@ -572,9 +695,21 @@ class Graphs(object):
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Music',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Live TV',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_2_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_3_output)
|
||||
if libraries.has_library_type('live'):
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': [series_1_output, series_2_output, series_3_output]}
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_per_stream_type(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||
@@ -592,7 +727,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
@@ -698,7 +833,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
@@ -784,7 +919,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
@@ -894,7 +1029,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
@@ -984,7 +1119,7 @@ class Graphs(object):
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
try:
|
||||
if y_axis == 'plays':
|
||||
|
@@ -13,7 +13,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import arrow
|
||||
import base64
|
||||
import certifi
|
||||
import cloudinary
|
||||
from cloudinary.api import delete_resources_by_tag
|
||||
from cloudinary.uploader import upload
|
||||
@@ -21,7 +23,6 @@ from cloudinary.utils import cloudinary_url
|
||||
import datetime
|
||||
from functools import wraps
|
||||
import geoip2.database, geoip2.errors
|
||||
import gzip
|
||||
import hashlib
|
||||
import imghdr
|
||||
from itertools import izip_longest
|
||||
@@ -34,12 +35,14 @@ from operator import itemgetter
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib, urllib2
|
||||
import urllib
|
||||
import urllib3
|
||||
from xml.dom import minidom
|
||||
import xmltodict
|
||||
|
||||
@@ -220,6 +223,22 @@ def utc_now_iso():
|
||||
return utcnow.isoformat()
|
||||
|
||||
|
||||
def timestamp_to_YMD(timestamp):
|
||||
return timestamp_to_datetime(timestamp).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def timestamp_to_datetime(timestamp):
|
||||
return datetime.datetime.fromtimestamp(cast_to_int(str(timestamp)))
|
||||
|
||||
|
||||
def iso_to_YMD(iso):
|
||||
return iso_to_datetime(iso).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def iso_to_datetime(iso):
|
||||
return arrow.get(iso).datetime
|
||||
|
||||
|
||||
def human_duration(s, sig='dhms'):
|
||||
|
||||
hd = ''
|
||||
@@ -421,7 +440,11 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
|
||||
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
|
||||
"""
|
||||
from OpenSSL import crypto
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
logger.error("Unable to generate self-signed certificates: Missing OpenSSL module.")
|
||||
return False
|
||||
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
||||
|
||||
serial = int(time.time())
|
||||
@@ -590,7 +613,8 @@ def update_geoip_db():
|
||||
now = int(time.time())
|
||||
if now - plexpy.CONFIG.GEOIP_DB_INSTALLED >= plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS * 24 * 60 * 60:
|
||||
return install_geoip_db(update=True)
|
||||
logger.info(u"Tautulli Helpers :: GeoLite2 database already updated within the last 30 days.")
|
||||
logger.info(u"Tautulli Helpers :: GeoLite2 database already updated within the last %s days."
|
||||
% plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS)
|
||||
|
||||
|
||||
def install_geoip_db(update=False):
|
||||
@@ -608,15 +632,21 @@ def install_geoip_db(update=False):
|
||||
geolite2_db = maxmind_db + '.mmdb'
|
||||
geolite2_db_path = plexpy.CONFIG.GEOIP_DB or os.path.join(plexpy.DATA_DIR, geolite2_db)
|
||||
|
||||
# Check path ends with .mmdb
|
||||
if os.path.splitext(geolite2_db_path)[1] != os.path.splitext(geolite2_db)[1]:
|
||||
geolite2_db_path = os.path.join(geolite2_db_path, geolite2_db)
|
||||
|
||||
temp_gz = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_gz)
|
||||
temp_md5 = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_md5)
|
||||
|
||||
# Retrieve the GeoLite2 gzip file
|
||||
logger.debug(u"Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
|
||||
try:
|
||||
maxmind = urllib.URLopener()
|
||||
maxmind.retrieve(geolite2_db_url, temp_gz)
|
||||
maxmind.retrieve(geolite2_md5_url, temp_md5)
|
||||
maxmind = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
|
||||
with maxmind.request('GET', geolite2_db_url, preload_content=False) as r_db, open(temp_gz, 'wb') as f_db:
|
||||
shutil.copyfileobj(r_db, f_db)
|
||||
with maxmind.request('GET', geolite2_md5_url, preload_content=False) as r_md5, open(temp_md5, 'wb') as f_md5:
|
||||
shutil.copyfileobj(r_md5, f_md5)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
|
||||
return False
|
||||
@@ -998,13 +1028,14 @@ def build_datatables_json(kwargs, dt_columns, default_sort_col=None):
|
||||
return json.dumps(json_data)
|
||||
|
||||
|
||||
def humanFileSize(bytes, si=False):
|
||||
def humanFileSize(bytes, si=True):
|
||||
if str(bytes).isdigit():
|
||||
bytes = int(bytes)
|
||||
bytes = cast_to_float(bytes)
|
||||
else:
|
||||
return bytes
|
||||
|
||||
thresh = 1000 if si else 1024
|
||||
#thresh = 1000 if si else 1024
|
||||
thresh = 1024 # Always divide by 2^10 but display SI units
|
||||
if bytes < thresh:
|
||||
return str(bytes) + ' B'
|
||||
|
||||
@@ -1232,3 +1263,94 @@ def mask_config_passwords(config):
|
||||
config[cfg] = ' '
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def bool_true(value, return_none=False):
|
||||
if value is None and return_none:
|
||||
return None
|
||||
elif value is True or value == 1:
|
||||
return True
|
||||
elif isinstance(value, basestring) and value.lower() in ('1', 'true', 't', 'yes', 'y', 'on'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def page(endpoint, *args, **kwargs):
|
||||
endpoints = {
|
||||
'pms_image_proxy': pms_image_proxy,
|
||||
'info': info_page,
|
||||
'library': library_page,
|
||||
'user': user_page
|
||||
}
|
||||
|
||||
params = {}
|
||||
|
||||
if endpoint in endpoints:
|
||||
params = endpoints[endpoint](*args, **kwargs)
|
||||
|
||||
return endpoint + '?' + urllib.urlencode(params)
|
||||
|
||||
|
||||
def pms_image_proxy(img=None, rating_key=None, width=None, height=None,
|
||||
opacity=None, background=None, blur=None, img_format=None,
|
||||
fallback=None, refresh=None, clip=None):
|
||||
params = {}
|
||||
|
||||
if img is not None:
|
||||
params['img'] = img
|
||||
if rating_key is not None:
|
||||
params['rating_key'] = rating_key
|
||||
if width is not None:
|
||||
params['width'] = width
|
||||
if height is not None:
|
||||
params['height'] = height
|
||||
if opacity is not None:
|
||||
params['opacity'] = opacity
|
||||
if background is not None:
|
||||
params['background'] = background
|
||||
if blur is not None:
|
||||
params['blur'] = blur
|
||||
if img_format is not None:
|
||||
params['img_format'] = img_format
|
||||
if fallback is not None:
|
||||
params['fallback'] = fallback
|
||||
if refresh is not None:
|
||||
params['refresh'] = 'true'
|
||||
if clip is not None:
|
||||
params['clip'] = 'true'
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def info_page(rating_key=None, guid=None, history=None, live=None):
|
||||
params = {}
|
||||
|
||||
if live and history:
|
||||
params['guid'] = guid
|
||||
else:
|
||||
params['rating_key'] = rating_key
|
||||
|
||||
if history:
|
||||
params['source'] = 'history'
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def library_page(section_id=None):
|
||||
params = {}
|
||||
|
||||
if section_id is not None:
|
||||
params['section_id'] = section_id
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def user_page(user_id=None, user=None):
|
||||
params = {}
|
||||
|
||||
if user_id is not None:
|
||||
params['user_id'] = user_id
|
||||
if user is not None:
|
||||
params['user'] = user
|
||||
|
||||
return params
|
||||
|
@@ -43,7 +43,12 @@ def refresh_libraries():
|
||||
library_keys = []
|
||||
new_keys = []
|
||||
|
||||
# Keep track of section_id to update is_active status
|
||||
section_ids = [common.LIVE_TV_SECTION_ID] # Live TV library always considered active
|
||||
|
||||
for section in library_sections:
|
||||
section_ids.append(helpers.cast_to_int(section['section_id']))
|
||||
|
||||
section_keys = {'server_id': server_id,
|
||||
'section_id': section['section_id']}
|
||||
section_values = {'server_id': server_id,
|
||||
@@ -65,6 +70,10 @@ def refresh_libraries():
|
||||
if result == 'insert':
|
||||
new_keys.append(section['section_id'])
|
||||
|
||||
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
|
||||
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
|
||||
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
|
||||
|
||||
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
||||
plexpy.CONFIG.write()
|
||||
@@ -88,6 +97,37 @@ def refresh_libraries():
|
||||
return False
|
||||
|
||||
|
||||
def add_live_tv_library():
|
||||
if not plexpy.CONFIG.ADD_LIVE_TV_LIBRARY:
|
||||
return
|
||||
|
||||
logger.info(u"Tautulli Libraries :: Adding Live TV library to the database.")
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'section_id': common.LIVE_TV_SECTION_ID}
|
||||
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'section_id': common.LIVE_TV_SECTION_ID,
|
||||
'section_name': common.LIVE_TV_SECTION_NAME,
|
||||
'section_type': 'live'
|
||||
}
|
||||
|
||||
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
|
||||
|
||||
if result == 'insert':
|
||||
plexpy.CONFIG.__setattr__('ADD_LIVE_TV_LIBRARY', 0)
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
|
||||
def has_library_type(section_type):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
query = 'SELECT * FROM library_sections WHERE section_type = ? AND deleted_section = 0'
|
||||
args = [section_type]
|
||||
result = monitor_db.select_single(query=query, args=args)
|
||||
return bool(result)
|
||||
|
||||
|
||||
def update_section_ids():
|
||||
plexpy.CONFIG.UPDATE_SECTION_IDS = -1
|
||||
|
||||
@@ -258,7 +298,9 @@ class Libraries(object):
|
||||
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
columns = ['library_sections.section_id',
|
||||
columns = ['library_sections.id AS row_id',
|
||||
'library_sections.server_id',
|
||||
'library_sections.section_id',
|
||||
'library_sections.section_name',
|
||||
'library_sections.section_type',
|
||||
'library_sections.count',
|
||||
@@ -272,7 +314,7 @@ class Libraries(object):
|
||||
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
|
||||
session_history.paused_counter END) AS duration',
|
||||
'MAX(session_history.started) AS last_accessed',
|
||||
'MAX(session_history.id) AS id',
|
||||
'MAX(session_history.id) AS history_row_id',
|
||||
'session_history_metadata.full_title AS last_played',
|
||||
'session_history.rating_key',
|
||||
'session_history_metadata.media_type',
|
||||
@@ -285,9 +327,14 @@ class Libraries(object):
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_metadata.content_rating',
|
||||
'session_history_metadata.labels',
|
||||
'session_history_metadata.live',
|
||||
'session_history_metadata.added_at',
|
||||
'session_history_metadata.originally_available_at',
|
||||
'session_history_metadata.guid',
|
||||
'library_sections.do_notify',
|
||||
'library_sections.do_notify_created',
|
||||
'library_sections.keep_history'
|
||||
'library_sections.keep_history',
|
||||
'library_sections.is_active'
|
||||
]
|
||||
try:
|
||||
query = data_tables.ssp_query(table_name='library_sections',
|
||||
@@ -326,7 +373,9 @@ class Libraries(object):
|
||||
else:
|
||||
library_thumb = common.DEFAULT_COVER_THUMB
|
||||
|
||||
row = {'section_id': item['section_id'],
|
||||
row = {'row_id': item['row_id'],
|
||||
'server_id': item['server_id'],
|
||||
'section_id': item['section_id'],
|
||||
'section_name': item['section_name'],
|
||||
'section_type': item['section_type'],
|
||||
'count': item['count'],
|
||||
@@ -337,7 +386,7 @@ class Libraries(object):
|
||||
'plays': item['plays'],
|
||||
'duration': item['duration'],
|
||||
'last_accessed': item['last_accessed'],
|
||||
'id': item['id'],
|
||||
'history_row_id': item['history_row_id'],
|
||||
'last_played': item['last_played'],
|
||||
'rating_key': item['rating_key'],
|
||||
'media_type': item['media_type'],
|
||||
@@ -348,9 +397,13 @@ class Libraries(object):
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'content_rating': item['content_rating'],
|
||||
'labels': item['labels'].split(';') if item['labels'] else (),
|
||||
'live': item['live'],
|
||||
'originally_available_at': item['originally_available_at'],
|
||||
'guid': item['guid'],
|
||||
'do_notify': helpers.checked(item['do_notify']),
|
||||
'do_notify_created': helpers.checked(item['do_notify_created']),
|
||||
'keep_history': helpers.checked(item['keep_history'])
|
||||
'keep_history': helpers.checked(item['keep_history']),
|
||||
'is_active': item['is_active']
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
@@ -670,12 +723,14 @@ class Libraries(object):
|
||||
|
||||
return True
|
||||
|
||||
def set_config(self, section_id=None, custom_thumb='', do_notify=1, keep_history=1, do_notify_created=1):
|
||||
def set_config(self, section_id=None, custom_thumb='', custom_art='',
|
||||
do_notify=1, keep_history=1, do_notify_created=1):
|
||||
if section_id:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
key_dict = {'section_id': section_id}
|
||||
value_dict = {'custom_thumb_url': custom_thumb,
|
||||
'custom_art_url': custom_art,
|
||||
'do_notify': do_notify,
|
||||
'do_notify_created': do_notify_created,
|
||||
'keep_history': keep_history}
|
||||
@@ -685,7 +740,9 @@ class Libraries(object):
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for set_config: %s." % e)
|
||||
|
||||
def get_details(self, section_id=None):
|
||||
default_return = {'section_id': 0,
|
||||
default_return = {'row_id': 0,
|
||||
'server_id': '',
|
||||
'section_id': 0,
|
||||
'section_name': 'Local',
|
||||
'section_type': '',
|
||||
'library_thumb': common.DEFAULT_COVER_THUMB,
|
||||
@@ -693,13 +750,14 @@ class Libraries(object):
|
||||
'count': 0,
|
||||
'parent_count': 0,
|
||||
'child_count': 0,
|
||||
'is_active': 1,
|
||||
'do_notify': 0,
|
||||
'do_notify_created': 0,
|
||||
'keep_history': 1,
|
||||
'deleted_section': 0
|
||||
}
|
||||
|
||||
if not section_id or helpers.cast_to_int(section_id) <= 0:
|
||||
if not section_id:
|
||||
return default_return
|
||||
|
||||
def get_library_details(section_id=section_id):
|
||||
@@ -707,8 +765,10 @@ class Libraries(object):
|
||||
|
||||
try:
|
||||
if str(section_id).isdigit():
|
||||
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
||||
query = 'SELECT id AS row_id, server_id, section_id, section_name, section_type, ' \
|
||||
'count, parent_count, child_count, ' \
|
||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art AS library_art, ' \
|
||||
'custom_art_url AS custom_art, is_active, ' \
|
||||
'do_notify, do_notify_created, keep_history, deleted_section ' \
|
||||
'FROM library_sections ' \
|
||||
'WHERE section_id = ? '
|
||||
@@ -729,14 +789,22 @@ class Libraries(object):
|
||||
else:
|
||||
library_thumb = common.DEFAULT_COVER_THUMB
|
||||
|
||||
library_details = {'section_id': item['section_id'],
|
||||
if item['custom_art'] and item['custom_art'] != item['library_art']:
|
||||
library_art = item['custom_art']
|
||||
else:
|
||||
library_art = item['library_art']
|
||||
|
||||
library_details = {'row_id': item['row_id'],
|
||||
'server_id': item['server_id'],
|
||||
'section_id': item['section_id'],
|
||||
'section_name': item['section_name'],
|
||||
'section_type': item['section_type'],
|
||||
'library_thumb': library_thumb,
|
||||
'library_art': item['art'],
|
||||
'library_art': library_art,
|
||||
'count': item['count'],
|
||||
'parent_count': item['parent_count'],
|
||||
'child_count': item['child_count'],
|
||||
'is_active': item['is_active'],
|
||||
'do_notify': item['do_notify'],
|
||||
'do_notify_created': item['do_notify_created'],
|
||||
'keep_history': item['keep_history'],
|
||||
@@ -766,21 +834,25 @@ class Libraries(object):
|
||||
# If there is no library data we must return something
|
||||
return default_return
|
||||
|
||||
def get_watch_time_stats(self, section_id=None, grouping=None):
|
||||
def get_watch_time_stats(self, section_id=None, grouping=None, query_days=None):
|
||||
if not session.allow_session_library(section_id):
|
||||
return []
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
if query_days and query_days is not None:
|
||||
query_days = map(helpers.cast_to_int, query_days.split(','))
|
||||
else:
|
||||
query_days = [1, 7, 30, 0]
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
time_queries = [1, 7, 30, 0]
|
||||
library_watch_time_stats = []
|
||||
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
for days in time_queries:
|
||||
for days in query_days:
|
||||
try:
|
||||
if days > 0:
|
||||
if str(section_id).isdigit():
|
||||
@@ -879,11 +951,11 @@ class Libraries(object):
|
||||
|
||||
try:
|
||||
if str(section_id).isdigit():
|
||||
query = 'SELECT session_history.id, session_history.media_type, ' \
|
||||
query = 'SELECT session_history.id, session_history.media_type, guid, ' \
|
||||
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
|
||||
'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, originally_available_at, added_at, live, started, user, content_rating, labels, section_id ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE section_id = ? ' \
|
||||
@@ -917,6 +989,9 @@ class Libraries(object):
|
||||
'media_index': row['media_index'],
|
||||
'parent_media_index': row['parent_media_index'],
|
||||
'year': row['year'],
|
||||
'originally_available_at': row['originally_available_at'],
|
||||
'live': row['live'],
|
||||
'guid': row['guid'],
|
||||
'time': row['started'],
|
||||
'user': row['user'],
|
||||
'section_id': row['section_id'],
|
||||
@@ -949,60 +1024,47 @@ class Libraries(object):
|
||||
|
||||
return libraries
|
||||
|
||||
def delete_all_history(self, section_id=None):
|
||||
def delete(self, server_id=None, section_id=None, row_ids=None, purge_only=False):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
if section_id.isdigit():
|
||||
logger.info(u"Tautulli Libraries :: Deleting all history for library id %s from database." % section_id)
|
||||
session_history_media_info_del = \
|
||||
monitor_db.action('DELETE FROM '
|
||||
'session_history_media_info '
|
||||
'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id '
|
||||
'FROM session_history_media_info '
|
||||
'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id '
|
||||
'WHERE session_history_metadata.section_id = ?)', [section_id])
|
||||
session_history_del = \
|
||||
monitor_db.action('DELETE FROM '
|
||||
'session_history '
|
||||
'WHERE session_history.id IN (SELECT session_history.id '
|
||||
'FROM session_history '
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id '
|
||||
'WHERE session_history_metadata.section_id = ?)', [section_id])
|
||||
session_history_metadata_del = \
|
||||
monitor_db.action('DELETE FROM '
|
||||
'session_history_metadata '
|
||||
'WHERE session_history_metadata.section_id = ?', [section_id])
|
||||
if row_ids and row_ids is not None:
|
||||
row_ids = map(helpers.cast_to_int, row_ids.split(','))
|
||||
|
||||
return 'Deleted all items for section_id %s.' % section_id
|
||||
# Get the user_ids corresponding to the row_ids
|
||||
result = monitor_db.select('SELECT server_id, section_id FROM library_sections '
|
||||
'WHERE id IN ({})'.format(','.join(['?'] * len(row_ids))), row_ids)
|
||||
|
||||
success = []
|
||||
for library in result:
|
||||
success.append(self.delete(server_id=library['server_id'], section_id=library['section_id'],
|
||||
purge_only=purge_only))
|
||||
return all(success)
|
||||
|
||||
elif str(section_id).isdigit():
|
||||
server_id = server_id or plexpy.CONFIG.PMS_IDENTIFIER
|
||||
if server_id == plexpy.CONFIG.PMS_IDENTIFIER:
|
||||
delete_success = database.delete_library_history(section_id=section_id)
|
||||
else:
|
||||
return 'Unable to delete items, section_id not valid.'
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for delete_all_history: %s." % e)
|
||||
logger.warn(u"Tautulli Libraries :: Library history not deleted for library section_id %s "
|
||||
u"because library server_id %s does not match Plex server identifier %s."
|
||||
% (section_id, server_id, plexpy.CONFIG.PMS_IDENTIFIER))
|
||||
delete_success = True
|
||||
|
||||
def delete(self, section_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
if section_id.isdigit():
|
||||
self.delete_all_history(section_id)
|
||||
logger.info(u"Tautulli Libraries :: Deleting library with id %s from database." % section_id)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 0 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 0 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 0 WHERE section_id = ?', [section_id])
|
||||
|
||||
library_cards = plexpy.CONFIG.HOME_LIBRARY_CARDS
|
||||
if section_id in library_cards:
|
||||
library_cards.remove(section_id)
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_cards)
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
return 'Deleted library with id %s.' % section_id
|
||||
if purge_only:
|
||||
return delete_success
|
||||
else:
|
||||
return 'Unable to delete library, section_id not valid.'
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for delete: %s." % e)
|
||||
logger.info(u"Tautulli Libraries :: Deleting library with server_id %s and section_id %s from database."
|
||||
% (server_id, section_id))
|
||||
try:
|
||||
monitor_db.action('UPDATE library_sections '
|
||||
'SET deleted_section = 1, keep_history = 0, do_notify = 0, do_notify_created = 0 '
|
||||
'WHERE server_id = ? AND section_id = ?', [server_id, section_id])
|
||||
return delete_success
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for delete: %s." % e)
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def undelete(self, section_id=None, section_name=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
@@ -1013,10 +1075,10 @@ class Libraries(object):
|
||||
result = monitor_db.select(query=query, args=[section_id])
|
||||
if result:
|
||||
logger.info(u"Tautulli Libraries :: Re-adding library with id %s to database." % section_id)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections '
|
||||
'SET deleted_section = 0, keep_history = 1, do_notify = 1, do_notify_created = 1 '
|
||||
'WHERE section_id = ?',
|
||||
[section_id])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -1026,10 +1088,10 @@ class Libraries(object):
|
||||
result = monitor_db.select(query=query, args=[section_name])
|
||||
if result:
|
||||
logger.info(u"Tautulli Libraries :: Re-adding library with name %s to database." % section_name)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections '
|
||||
'SET deleted_section = 0, keep_history = 1, do_notify = 1, do_notify_created = 1 '
|
||||
'WHERE section_name = ?',
|
||||
[section_name])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@@ -80,7 +80,7 @@ class BlacklistFilter(logging.Filter):
|
||||
Log filter for blacklisted tokens and passwords
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
super(BlacklistFilter, self).__init__()
|
||||
|
||||
def filter(self, record):
|
||||
if not plexpy.CONFIG.LOG_BLACKLIST:
|
||||
@@ -98,30 +98,29 @@ class BlacklistFilter(logging.Filter):
|
||||
return True
|
||||
|
||||
|
||||
class PublicIPFilter(logging.Filter):
|
||||
class RegexFilter(logging.Filter):
|
||||
"""
|
||||
Log filter for public IP addresses
|
||||
Base class for regex log filter
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
super(RegexFilter, self).__init__()
|
||||
|
||||
self.regex = re.compile(r'')
|
||||
|
||||
def filter(self, record):
|
||||
if not plexpy.CONFIG.LOG_BLACKLIST:
|
||||
return True
|
||||
|
||||
try:
|
||||
# Currently only checking for ipv4 addresses
|
||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg)
|
||||
for ip in ipv4:
|
||||
if helpers.is_public_ip(ip):
|
||||
record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
||||
matches = self.regex.findall(record.msg)
|
||||
for match in matches:
|
||||
record.msg = self.replace(record.msg, match)
|
||||
|
||||
args = []
|
||||
for arg in record.args:
|
||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', arg) if isinstance(arg, basestring) else []
|
||||
for ip in ipv4:
|
||||
if helpers.is_public_ip(ip):
|
||||
arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
||||
matches = self.regex.findall(arg) if isinstance(arg, basestring) else []
|
||||
for match in matches:
|
||||
arg = self.replace(arg, match)
|
||||
args.append(arg)
|
||||
record.args = tuple(args)
|
||||
except:
|
||||
@@ -129,31 +128,54 @@ class PublicIPFilter(logging.Filter):
|
||||
|
||||
return True
|
||||
|
||||
def replace(self, text, match):
|
||||
return text
|
||||
|
||||
class PlexTokenFilter(logging.Filter):
|
||||
|
||||
class PublicIPFilter(RegexFilter):
|
||||
"""
|
||||
Log filter for public IP addresses
|
||||
"""
|
||||
def __init__(self):
|
||||
super(PublicIPFilter, self).__init__()
|
||||
|
||||
# Currently only checking for ipv4 addresses
|
||||
self.regex = re.compile(r'[0-9]+(?:[.-][0-9]+){3}(?!\d*-[a-z0-9]{6})')
|
||||
|
||||
def replace(self, text, ip):
|
||||
if helpers.is_public_ip(ip.replace('-', '.')):
|
||||
partition = '-' if '-' in ip else '.'
|
||||
return text.replace(ip, ip.partition(partition)[0] + (partition + '***') * 3)
|
||||
return text
|
||||
|
||||
|
||||
class EmailFilter(RegexFilter):
|
||||
"""
|
||||
Log filter for email addresses
|
||||
"""
|
||||
def __init__(self):
|
||||
super(EmailFilter, self).__init__()
|
||||
|
||||
self.regex = re.compile(r'([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@'
|
||||
r'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)',
|
||||
re.IGNORECASE)
|
||||
|
||||
def replace(self, text, email):
|
||||
email_parts = email.partition('@')
|
||||
return text.replace(email, email_parts[0][:2] + 8 * '*' + email_parts[1] + 8 * '*')
|
||||
|
||||
|
||||
class PlexTokenFilter(RegexFilter):
|
||||
"""
|
||||
Log filter for X-Plex-Token
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
super(PlexTokenFilter, self).__init__()
|
||||
|
||||
def filter(self, record):
|
||||
try:
|
||||
tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', record.msg)
|
||||
for token in tokens:
|
||||
record.msg = record.msg.replace(token, 8 * '*' + token[-2:])
|
||||
self.regex = re.compile(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)')
|
||||
|
||||
args = []
|
||||
for arg in record.args:
|
||||
tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', arg) if isinstance(arg, basestring) else []
|
||||
for token in tokens:
|
||||
arg = arg.replace(token, 8 * '*' + token[-2:])
|
||||
args.append(arg)
|
||||
record.args = tuple(args)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
def replace(self, text, token):
|
||||
return text.replace(token, 8 * '*' + token[-2:])
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -294,6 +316,7 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||
for handler in logger.handlers + logger_api.handlers + logger_plex_websocket.handlers:
|
||||
handler.addFilter(BlacklistFilter())
|
||||
handler.addFilter(PublicIPFilter())
|
||||
handler.addFilter(EmailFilter())
|
||||
handler.addFilter(PlexTokenFilter())
|
||||
|
||||
# Install exception hooks
|
||||
|
@@ -14,6 +14,7 @@
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import threading
|
||||
|
||||
import plexpy
|
||||
import database
|
||||
@@ -22,6 +23,24 @@ import logger
|
||||
|
||||
|
||||
TEMP_DEVICE_TOKEN = None
|
||||
INVALIDATE_TIMER = None
|
||||
|
||||
|
||||
def set_temp_device_token(token=None):
|
||||
global TEMP_DEVICE_TOKEN
|
||||
TEMP_DEVICE_TOKEN = token
|
||||
|
||||
if TEMP_DEVICE_TOKEN is not None:
|
||||
global INVALIDATE_TIMER
|
||||
if INVALIDATE_TIMER:
|
||||
INVALIDATE_TIMER.cancel()
|
||||
invalidate_time = 5 * 60 # 5 minutes
|
||||
INVALIDATE_TIMER = threading.Timer(invalidate_time, set_temp_device_token, args=[None])
|
||||
INVALIDATE_TIMER.start()
|
||||
|
||||
|
||||
def get_temp_device_token():
|
||||
return TEMP_DEVICE_TOKEN
|
||||
|
||||
|
||||
def get_mobile_devices(device_id=None, device_token=None):
|
||||
@@ -97,10 +116,7 @@ def set_mobile_device_config(mobile_device_id=None, **kwargs):
|
||||
return False
|
||||
|
||||
keys = {'id': mobile_device_id}
|
||||
values = {}
|
||||
|
||||
if kwargs.get('friendly_name'):
|
||||
values['friendly_name'] = kwargs['friendly_name']
|
||||
values = {'friendly_name': kwargs.get('friendly_name', '')}
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
try:
|
||||
|
@@ -284,7 +284,7 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
|
||||
email_config=newsletter_config['email_config'],
|
||||
subject=subject,
|
||||
body=body,
|
||||
messsage=message)
|
||||
message=message)
|
||||
return agent.send()
|
||||
else:
|
||||
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
|
||||
|
@@ -536,8 +536,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
sessions = ap.get_sessions()
|
||||
stream_count = len(sessions)
|
||||
user_sessions = ap.get_sessions(user_id=session.get('user_id'))
|
||||
|
||||
# Filter out the session_key from the database sessions for playback stopped events
|
||||
# to prevent race condition between the database and notifications
|
||||
if notify_action == 'on_stop':
|
||||
sessions = [s for s in sessions if str(s['session_key']) != notify_params['session_key']]
|
||||
user_sessions = [s for s in user_sessions if str(s['session_key']) != notify_params['session_key']]
|
||||
|
||||
stream_count = len(sessions)
|
||||
user_stream_count = len(user_sessions)
|
||||
|
||||
# Generate a combined transcode decision value
|
||||
@@ -547,6 +554,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
transcode_decision = 'Direct Stream'
|
||||
else:
|
||||
transcode_decision = 'Direct Play'
|
||||
transcode_decision_count = Counter(s['transcode_decision'] for s in sessions)
|
||||
user_transcode_decision_count = Counter(s['transcode_decision'] for s in user_sessions)
|
||||
|
||||
if notify_action != 'on_play':
|
||||
stream_duration = int((time.time() -
|
||||
@@ -683,18 +692,26 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
poster_key = notify_params['parent_rating_key']
|
||||
poster_title = '%s - %s' % (notify_params['grandparent_title'],
|
||||
notify_params['parent_title'])
|
||||
elif notify_params['media_type'] == 'clip':
|
||||
if notify_params['extra_type']:
|
||||
poster_thumb = notify_params['art'].replace('/art', '/thumb') or notify_params['thumb']
|
||||
else:
|
||||
poster_thumb = notify_params['parent_thumb'] or notify_params['thumb']
|
||||
poster_key = notify_params['rating_key']
|
||||
poster_title = notify_params['title']
|
||||
else:
|
||||
poster_thumb = ''
|
||||
poster_key = ''
|
||||
poster_title = ''
|
||||
|
||||
img_service = helpers.get_img_service(include_self=True)
|
||||
fallback = 'poster-live' if notify_params['live'] else 'poster'
|
||||
if img_service not in (None, 'self-hosted'):
|
||||
img_info = get_img_info(img=poster_thumb, rating_key=poster_key, title=poster_title, fallback='poster')
|
||||
img_info = get_img_info(img=poster_thumb, rating_key=poster_key, title=poster_title, fallback=fallback)
|
||||
poster_info = {'poster_title': img_info['img_title'], 'poster_url': img_info['img_url']}
|
||||
notify_params.update(poster_info)
|
||||
elif img_service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
img_hash = set_hash_image_info(img=poster_thumb, fallback='poster')
|
||||
img_hash = set_hash_image_info(img=poster_thumb, fallback=fallback)
|
||||
poster_info = {'poster_title': poster_title,
|
||||
'poster_url': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/' + img_hash}
|
||||
notify_params.update(poster_info)
|
||||
@@ -793,7 +810,13 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'utctime': helpers.utc_now_iso(),
|
||||
# Stream parameters
|
||||
'streams': stream_count,
|
||||
'direct_plays': transcode_decision_count['direct play'],
|
||||
'direct_streams': transcode_decision_count['copy'],
|
||||
'transcodes': transcode_decision_count['transcode'],
|
||||
'user_streams': user_stream_count,
|
||||
'user_direct_plays': user_transcode_decision_count['direct play'],
|
||||
'user_direct_streams': user_transcode_decision_count['copy'],
|
||||
'user_transcodes': user_transcode_decision_count['transcode'],
|
||||
'user': notify_params['friendly_name'],
|
||||
'username': notify_params['user'],
|
||||
'user_email': notify_params['email'],
|
||||
@@ -818,6 +841,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'optimized_version_profile': notify_params['optimized_version_profile'],
|
||||
'synced_version': notify_params['synced_version'],
|
||||
'live': notify_params['live'],
|
||||
'channel_call_sign': notify_params['channel_call_sign'],
|
||||
'channel_identifier': notify_params['channel_identifier'],
|
||||
'channel_thumb': notify_params['channel_thumb'],
|
||||
'secure': 'unknown' if notify_params['secure'] is None else notify_params['secure'],
|
||||
'relayed': notify_params['relayed'],
|
||||
'stream_local': notify_params['local'],
|
||||
@@ -830,6 +856,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'stream_video_codec_level': notify_params['stream_video_codec_level'],
|
||||
'stream_video_bitrate': notify_params['stream_video_bitrate'],
|
||||
'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
|
||||
'stream_video_chroma_subsampling': notify_params['stream_video_chroma_subsampling'],
|
||||
'stream_video_color_primaries': notify_params['stream_video_color_primaries'],
|
||||
'stream_video_color_range': notify_params['stream_video_color_range'],
|
||||
'stream_video_color_space': notify_params['stream_video_color_space'],
|
||||
'stream_video_color_trc': notify_params['stream_video_color_trc'],
|
||||
'stream_video_dynamic_range': notify_params['stream_video_dynamic_range'],
|
||||
'stream_video_framerate': notify_params['stream_video_framerate'],
|
||||
'stream_video_full_resolution': notify_params['stream_video_full_resolution'],
|
||||
'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
|
||||
@@ -940,6 +972,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'video_codec_level': notify_params['video_codec_level'],
|
||||
'video_bitrate': notify_params['video_bitrate'],
|
||||
'video_bit_depth': notify_params['video_bit_depth'],
|
||||
'video_chroma_subsampling': notify_params['video_chroma_subsampling'],
|
||||
'video_color_primaries': notify_params['video_color_primaries'],
|
||||
'video_color_range': notify_params['video_color_range'],
|
||||
'video_color_space': notify_params['video_color_space'],
|
||||
'video_color_trc': notify_params['video_color_trc'],
|
||||
'video_dynamic_range': notify_params['video_dynamic_range'],
|
||||
'video_framerate': notify_params['video_framerate'],
|
||||
'video_full_resolution': notify_params['video_full_resolution'],
|
||||
'video_ref_frames': notify_params['video_ref_frames'],
|
||||
@@ -972,6 +1010,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'rating_key': notify_params['rating_key'],
|
||||
'parent_rating_key': notify_params['parent_rating_key'],
|
||||
'grandparent_rating_key': notify_params['grandparent_rating_key'],
|
||||
'art': notify_params['art'],
|
||||
'thumb': notify_params['thumb'],
|
||||
'parent_thumb': notify_params['parent_thumb'],
|
||||
'grandparent_thumb': notify_params['grandparent_thumb'],
|
||||
@@ -1178,10 +1217,6 @@ def strip_tag(data, agent_id=None):
|
||||
'font': ['color']}
|
||||
data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
||||
|
||||
elif agent_id in (10, 14, 20):
|
||||
# Don't remove tags for Email, Slack, and Discord
|
||||
pass
|
||||
|
||||
elif agent_id == 13:
|
||||
# Allow tags b, i, code, pre, a[href] for Telegram
|
||||
whitelist = {'b': [],
|
||||
@@ -1191,6 +1226,10 @@ def strip_tag(data, agent_id=None):
|
||||
'a': ['href']}
|
||||
data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
||||
|
||||
elif agent_id in (10, 14, 20, 25):
|
||||
# Don't remove tags for Email, Slack, Discord, and Webhook
|
||||
pass
|
||||
|
||||
else:
|
||||
whitelist = {}
|
||||
data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
||||
@@ -1227,14 +1266,17 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
|
||||
return img_info
|
||||
|
||||
if rating_key and not img:
|
||||
if fallback == 'art':
|
||||
if fallback and fallback.startswith('art'):
|
||||
img = '/library/metadata/{}/art'.format(rating_key)
|
||||
else:
|
||||
img = '/library/metadata/{}/thumb'.format(rating_key)
|
||||
|
||||
img_split = img.split('/')
|
||||
img = '/'.join(img_split[:5])
|
||||
rating_key = rating_key or img_split[3]
|
||||
if img.startswith('/library/metadata'):
|
||||
img_split = img.split('/')
|
||||
img = '/'.join(img_split[:5])
|
||||
img_rating_key = img_split[3]
|
||||
if rating_key != img_rating_key:
|
||||
rating_key = img_rating_key
|
||||
|
||||
service = helpers.get_img_service()
|
||||
|
||||
@@ -1244,7 +1286,7 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
|
||||
elif service == 'cloudinary':
|
||||
if fallback == 'cover':
|
||||
w, h = 1000, 1000
|
||||
elif fallback == 'art':
|
||||
elif fallback and fallback.startswith('art'):
|
||||
w, h = 1920, 1080
|
||||
else:
|
||||
w, h = 1000, 1500
|
||||
@@ -1328,14 +1370,17 @@ def set_hash_image_info(img=None, rating_key=None, width=750, height=1000,
|
||||
return fallback
|
||||
|
||||
if rating_key and not img:
|
||||
if fallback == 'art':
|
||||
if fallback and fallback.startswith('art'):
|
||||
img = '/library/metadata/{}/art'.format(rating_key)
|
||||
else:
|
||||
img = '/library/metadata/{}/thumb'.format(rating_key)
|
||||
|
||||
img_split = img.split('/')
|
||||
img = '/'.join(img_split[:5])
|
||||
rating_key = rating_key or img_split[3]
|
||||
if img.startswith('/library/metadata'):
|
||||
img_split = img.split('/')
|
||||
img = '/'.join(img_split[:5])
|
||||
img_rating_key = img_split[3]
|
||||
if rating_key != img_rating_key:
|
||||
rating_key = img_rating_key
|
||||
|
||||
img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
|
||||
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
|
||||
|
@@ -339,6 +339,14 @@ def available_notification_actions():
|
||||
'body': 'An update is available for Tautulli (version {tautulli_update_version}).',
|
||||
'icon': 'fa-refresh',
|
||||
'media_types': ('server',)
|
||||
},
|
||||
{'label': 'Tautulli Database Corruption',
|
||||
'name': 'on_plexpydbcorrupt',
|
||||
'description': 'Trigger a notification if Tautulli database corruption is detected when backing up the database.',
|
||||
'subject': 'Tautulli ({server_name})',
|
||||
'body': 'Tautulli database corruption detected. Automatic cleanup of database backups is suspended.',
|
||||
'icon': 'fa-database',
|
||||
'media_types': ('server',)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3060,7 +3068,8 @@ class SCRIPTS(Notifier):
|
||||
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
|
||||
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
|
||||
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
|
||||
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING
|
||||
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
|
||||
'TAUTULLI_PYTHON_VERSION': '.'.join(map(str, plexpy.PYTHON_VERSION))
|
||||
})
|
||||
|
||||
if user_id:
|
||||
@@ -3416,9 +3425,9 @@ class TELEGRAM(Notifier):
|
||||
data = {'chat_id': self.config['chat_id']}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
text = subject + '\r\n' + body
|
||||
else:
|
||||
text = body.encode('utf-8')
|
||||
text = body
|
||||
|
||||
if self.config['html_support']:
|
||||
data['parse_mode'] = 'HTML'
|
||||
@@ -3442,7 +3451,7 @@ class TELEGRAM(Notifier):
|
||||
if len(text) > 1024:
|
||||
data['disable_notification'] = True
|
||||
else:
|
||||
data['caption'] = text
|
||||
data['caption'] = text.encode('utf-8')
|
||||
|
||||
r = self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
|
||||
data=data, files=files)
|
||||
@@ -3450,7 +3459,7 @@ class TELEGRAM(Notifier):
|
||||
if not data.pop('disable_notification', None):
|
||||
return r
|
||||
|
||||
data['text'] = text
|
||||
data['text'] = (text[:4093] + (text[4093:] and '...')).encode('utf-8')
|
||||
|
||||
if self.config['disable_web_preview']:
|
||||
data['disable_web_page_preview'] = True
|
||||
@@ -3609,7 +3618,7 @@ class WEBHOOK(Notifier):
|
||||
"""
|
||||
NAME = 'Webhook'
|
||||
_DEFAULT_CONFIG = {'hook': '',
|
||||
'method': ''
|
||||
'method': 'POST'
|
||||
}
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
@@ -3636,7 +3645,12 @@ class WEBHOOK(Notifier):
|
||||
if webhook_headers:
|
||||
headers.update(webhook_headers)
|
||||
|
||||
return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, json=webhook_body)
|
||||
if headers['Content-Type'] == 'application/json':
|
||||
data = {'json': webhook_body}
|
||||
else:
|
||||
data = {'data': webhook_body}
|
||||
|
||||
return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, **data)
|
||||
|
||||
def _return_config_options(self):
|
||||
config_option = [{'label': 'Webhook URL',
|
||||
@@ -3650,8 +3664,7 @@ class WEBHOOK(Notifier):
|
||||
'name': 'webhook_method',
|
||||
'description': 'The Webhook HTTP request method.',
|
||||
'input_type': 'select',
|
||||
'select_options': {'': '',
|
||||
'GET': 'GET',
|
||||
'select_options': {'GET': 'GET',
|
||||
'POST': 'POST',
|
||||
'PUT': 'PUT',
|
||||
'DELETE': 'DELETE'}
|
||||
|
@@ -396,6 +396,7 @@ class PlexTV(object):
|
||||
"username": helpers.get_xml_attr(a, 'username'),
|
||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||
"email": helpers.get_xml_attr(a, 'email'),
|
||||
"is_active": 1,
|
||||
"is_admin": 1,
|
||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||
"is_allow_sync": 1,
|
||||
@@ -423,6 +424,7 @@ class PlexTV(object):
|
||||
"username": helpers.get_xml_attr(a, 'title'),
|
||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||
"email": helpers.get_xml_attr(a, 'email'),
|
||||
"is_active": 1,
|
||||
"is_admin": 0,
|
||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
|
||||
|
@@ -571,7 +571,8 @@ class PmsConnect(object):
|
||||
|
||||
return output
|
||||
|
||||
def get_metadata_details(self, rating_key='', sync_id='', cache_key=None, media_info=True):
|
||||
def get_metadata_details(self, rating_key='', sync_id='', plex_guid='',
|
||||
skip_cache=False, cache_key=None, return_cache=False, media_info=True):
|
||||
"""
|
||||
Return processed and validated metadata list for requested item.
|
||||
|
||||
@@ -581,7 +582,7 @@ class PmsConnect(object):
|
||||
"""
|
||||
metadata = {}
|
||||
|
||||
if cache_key:
|
||||
if not skip_cache and cache_key:
|
||||
in_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||
in_file_path = os.path.join(in_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||
|
||||
@@ -596,14 +597,18 @@ class PmsConnect(object):
|
||||
|
||||
if metadata:
|
||||
_cache_time = metadata.pop('_cache_time', 0)
|
||||
# Return cached metadata if less than METADATA_CACHE_SECONDS ago
|
||||
if int(time.time()) - _cache_time <= plexpy.CONFIG.METADATA_CACHE_SECONDS:
|
||||
# Return cached metadata if less than cache_seconds ago
|
||||
if return_cache or int(time.time()) - _cache_time <= plexpy.CONFIG.METADATA_CACHE_SECONDS:
|
||||
return metadata
|
||||
|
||||
if rating_key:
|
||||
metadata_xml = self.get_metadata(str(rating_key), output_format='xml')
|
||||
elif sync_id:
|
||||
metadata_xml = self.get_sync_item(str(sync_id), output_format='xml')
|
||||
elif plex_guid.startswith(('plex://movie', 'plex://episode')):
|
||||
rating_key = plex_guid.rsplit('/', 1)[-1]
|
||||
plextv_metadata = PmsConnect(url='https://metadata.provider.plex.tv', token=plexpy.CONFIG.PMS_TOKEN)
|
||||
metadata_xml = plextv_metadata.get_metadata(rating_key, output_format='xml')
|
||||
else:
|
||||
return metadata
|
||||
|
||||
@@ -719,7 +724,8 @@ class PmsConnect(object):
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'show':
|
||||
@@ -771,12 +777,19 @@ class PmsConnect(object):
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'season':
|
||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||
show_details = self.get_metadata_details(parent_rating_key)
|
||||
parent_guid = helpers.get_xml_attr(metadata_main, 'parentGuid')
|
||||
show_details = {}
|
||||
if plex_guid and parent_guid:
|
||||
show_details = self.get_metadata_details(plex_guid=parent_guid)
|
||||
elif not plex_guid and parent_rating_key:
|
||||
show_details = self.get_metadata_details(parent_rating_key)
|
||||
|
||||
metadata = {'media_type': metadata_type,
|
||||
'section_id': section_id,
|
||||
'library_name': library_name,
|
||||
@@ -790,22 +803,22 @@ class PmsConnect(object):
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
'studio': show_details['studio'],
|
||||
'content_rating': show_details['content_rating'],
|
||||
'summary': show_details['summary'],
|
||||
'studio': show_details.get('studio', ''),
|
||||
'content_rating': show_details.get('content_rating', ''),
|
||||
'summary': show_details.get('summary', ''),
|
||||
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
|
||||
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
|
||||
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
|
||||
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
|
||||
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
|
||||
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
|
||||
'duration': show_details['duration'],
|
||||
'duration': show_details.get('duration', ''),
|
||||
'year': helpers.get_xml_attr(metadata_main, 'year'),
|
||||
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
|
||||
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': show_details['banner'],
|
||||
'banner': show_details.get('banner', ''),
|
||||
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
|
||||
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
|
||||
@@ -813,32 +826,38 @@ class PmsConnect(object):
|
||||
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
|
||||
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
|
||||
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
|
||||
'directors': show_details['directors'],
|
||||
'writers': show_details['writers'],
|
||||
'actors': show_details['actors'],
|
||||
'genres': show_details['genres'],
|
||||
'labels': show_details['labels'],
|
||||
'collections': show_details['collections'],
|
||||
'directors': show_details.get('directors', []),
|
||||
'writers': show_details.get('writers', []),
|
||||
'actors': show_details.get('actors', []),
|
||||
'genres': show_details.get('genres', []),
|
||||
'labels': show_details.get('labels', []),
|
||||
'collections': show_details.get('collections', []),
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
helpers.get_xml_attr(metadata_main, 'title')),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'episode':
|
||||
grandparent_rating_key = helpers.get_xml_attr(metadata_main, 'grandparentRatingKey')
|
||||
show_details = self.get_metadata_details(grandparent_rating_key)
|
||||
grandparent_guid = helpers.get_xml_attr(metadata_main, 'grandparentGuid')
|
||||
show_details = {}
|
||||
if plex_guid and grandparent_guid:
|
||||
show_details = self.get_metadata_details(plex_guid=grandparent_guid)
|
||||
elif not plex_guid and grandparent_rating_key:
|
||||
show_details = self.get_metadata_details(grandparent_rating_key)
|
||||
|
||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||
parent_media_index = helpers.get_xml_attr(metadata_main, 'parentIndex')
|
||||
parent_thumb = helpers.get_xml_attr(metadata_main, 'parentThumb')
|
||||
|
||||
if not parent_rating_key:
|
||||
if not plex_guid and not parent_rating_key:
|
||||
# Try getting the parent_rating_key from the parent_thumb
|
||||
if parent_thumb.startswith('/library/metadata/'):
|
||||
parent_rating_key = parent_thumb.split('/')[3]
|
||||
|
||||
# Try getting the parent_rating_key from the grandparent's children
|
||||
if not parent_rating_key:
|
||||
if not parent_rating_key and grandparent_rating_key:
|
||||
children_list = self.get_item_children(grandparent_rating_key)
|
||||
parent_rating_key = next((c['rating_key'] for c in children_list['children_list']
|
||||
if c['media_index'] == parent_media_index), '')
|
||||
@@ -856,7 +875,7 @@ class PmsConnect(object):
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': parent_media_index,
|
||||
'studio': show_details['studio'],
|
||||
'studio': show_details.get('studio', ''),
|
||||
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
|
||||
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
|
||||
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
|
||||
@@ -871,7 +890,7 @@ class PmsConnect(object):
|
||||
'parent_thumb': parent_thumb,
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': show_details['banner'],
|
||||
'banner': show_details.get('banner', ''),
|
||||
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
|
||||
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
|
||||
@@ -881,13 +900,14 @@ class PmsConnect(object):
|
||||
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
|
||||
'directors': directors,
|
||||
'writers': writers,
|
||||
'actors': show_details['actors'],
|
||||
'genres': show_details['genres'],
|
||||
'labels': show_details['labels'],
|
||||
'collections': show_details['collections'],
|
||||
'actors': show_details.get('actors', []),
|
||||
'genres': show_details.get('genres', []),
|
||||
'labels': show_details.get('labels', []),
|
||||
'collections': show_details.get('collections', []),
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
helpers.get_xml_attr(metadata_main, 'title')),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'artist':
|
||||
@@ -934,12 +954,13 @@ class PmsConnect(object):
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'album':
|
||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||
artist_details = self.get_metadata_details(parent_rating_key)
|
||||
artist_details = self.get_metadata_details(parent_rating_key) if parent_rating_key else {}
|
||||
metadata = {'media_type': metadata_type,
|
||||
'section_id': section_id,
|
||||
'library_name': library_name,
|
||||
@@ -955,7 +976,7 @@ class PmsConnect(object):
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
|
||||
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
|
||||
'summary': helpers.get_xml_attr(metadata_main, 'summary') or artist_details['summary'],
|
||||
'summary': helpers.get_xml_attr(metadata_main, 'summary') or artist_details.get('summary', ''),
|
||||
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
|
||||
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
|
||||
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
|
||||
@@ -968,7 +989,7 @@ class PmsConnect(object):
|
||||
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': artist_details['banner'],
|
||||
'banner': artist_details.get('banner', ''),
|
||||
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
|
||||
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
|
||||
@@ -984,12 +1005,13 @@ class PmsConnect(object):
|
||||
'collections': collections,
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
helpers.get_xml_attr(metadata_main, 'title')),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'track':
|
||||
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) if parent_rating_key else {}
|
||||
track_artist = helpers.get_xml_attr(metadata_main, 'originalTitle') or \
|
||||
helpers.get_xml_attr(metadata_main, 'grandparentTitle')
|
||||
metadata = {'media_type': metadata_type,
|
||||
@@ -1015,12 +1037,12 @@ class PmsConnect(object):
|
||||
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
|
||||
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
|
||||
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
|
||||
'year': album_details['year'],
|
||||
'year': album_details.get('year', ''),
|
||||
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
|
||||
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': album_details['banner'],
|
||||
'banner': album_details.get('banner', ''),
|
||||
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
|
||||
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
|
||||
@@ -1031,12 +1053,13 @@ class PmsConnect(object):
|
||||
'directors': directors,
|
||||
'writers': writers,
|
||||
'actors': actors,
|
||||
'genres': album_details['genres'],
|
||||
'labels': album_details['labels'],
|
||||
'collections': album_details['collections'],
|
||||
'genres': album_details.get('genres', []),
|
||||
'labels': album_details.get('labels', []),
|
||||
'collections': album_details.get('collections', []),
|
||||
'full_title': u'{} - {}'.format(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'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'photo_album':
|
||||
@@ -1083,12 +1106,13 @@ class PmsConnect(object):
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'photo':
|
||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||
photo_album_details = self.get_metadata_details(parent_rating_key)
|
||||
photo_album_details = self.get_metadata_details(parent_rating_key) if parent_rating_key else {}
|
||||
metadata = {'media_type': metadata_type,
|
||||
'section_id': section_id,
|
||||
'library_name': library_name,
|
||||
@@ -1128,12 +1152,13 @@ class PmsConnect(object):
|
||||
'directors': directors,
|
||||
'writers': writers,
|
||||
'actors': actors,
|
||||
'genres': photo_album_details.get('genres', ''),
|
||||
'labels': photo_album_details.get('labels', ''),
|
||||
'collections': photo_album_details.get('collections', ''),
|
||||
'genres': photo_album_details.get('genres', []),
|
||||
'labels': photo_album_details.get('labels', []),
|
||||
'collections': photo_album_details.get('collections', []),
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name,
|
||||
helpers.get_xml_attr(metadata_main, 'title')),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'collection':
|
||||
@@ -1184,7 +1209,8 @@ class PmsConnect(object):
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
elif metadata_type == 'clip':
|
||||
@@ -1232,17 +1258,31 @@ class PmsConnect(object):
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'extra_type': helpers.get_xml_attr(metadata_main, 'extraType'),
|
||||
'sub_type': helpers.get_xml_attr(metadata_main, 'subtype')
|
||||
'sub_type': helpers.get_xml_attr(metadata_main, 'subtype'),
|
||||
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
|
||||
}
|
||||
|
||||
else:
|
||||
return metadata
|
||||
|
||||
# Get additional metadata from metadata.provider.plex.tv
|
||||
if not plex_guid and metadata['live']:
|
||||
metadata['section_id'] = common.LIVE_TV_SECTION_ID
|
||||
metadata['library_name'] = common.LIVE_TV_SECTION_NAME
|
||||
|
||||
plextv_metadata = self.get_metadata_details(plex_guid=metadata['guid'])
|
||||
if plextv_metadata:
|
||||
keys_to_update = ['summary', 'rating', 'thumb', 'grandparent_thumb', 'duration',
|
||||
'guid', 'grandparent_guid', 'genres']
|
||||
for key in keys_to_update:
|
||||
metadata[key] = plextv_metadata[key]
|
||||
metadata['originally_available_at'] = helpers.iso_to_YMD(plextv_metadata['originally_available_at'])
|
||||
|
||||
if metadata and media_info:
|
||||
medias = []
|
||||
media_items = metadata_main.getElementsByTagName('Media')
|
||||
for media in media_items:
|
||||
video_full_resolution_scan_type = None
|
||||
video_full_resolution_scan_type = ''
|
||||
|
||||
parts = []
|
||||
part_items = media.getElementsByTagName('Part')
|
||||
@@ -1253,8 +1293,7 @@ class PmsConnect(object):
|
||||
for stream in stream_items:
|
||||
if helpers.get_xml_attr(stream, 'streamType') == '1':
|
||||
video_scan_type = helpers.get_xml_attr(stream, 'scanType')
|
||||
if video_full_resolution_scan_type is None:
|
||||
video_full_resolution_scan_type = video_scan_type
|
||||
video_full_resolution_scan_type = (video_full_resolution_scan_type or video_scan_type)
|
||||
|
||||
streams.append({'id': helpers.get_xml_attr(stream, 'id'),
|
||||
'type': helpers.get_xml_attr(stream, 'streamType'),
|
||||
@@ -1262,6 +1301,11 @@ class PmsConnect(object):
|
||||
'video_codec_level': helpers.get_xml_attr(stream, 'level'),
|
||||
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
|
||||
'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'),
|
||||
'video_chroma_subsampling': helpers.get_xml_attr(stream, 'chromaSubsampling'),
|
||||
'video_color_primaries': helpers.get_xml_attr(stream, 'colorPrimaries'),
|
||||
'video_color_range': helpers.get_xml_attr(stream, 'colorRange'),
|
||||
'video_color_space': helpers.get_xml_attr(stream, 'colorSpace'),
|
||||
'video_color_trc': helpers.get_xml_attr(stream, 'colorTrc'),
|
||||
'video_frame_rate': helpers.get_xml_attr(stream, 'frameRate'),
|
||||
'video_ref_frames': helpers.get_xml_attr(stream, 'refFrames'),
|
||||
'video_height': helpers.get_xml_attr(stream, 'height'),
|
||||
@@ -1309,35 +1353,36 @@ class PmsConnect(object):
|
||||
'selected': int(helpers.get_xml_attr(part, 'selected') == '1')
|
||||
})
|
||||
|
||||
video_resolution = helpers.get_xml_attr(media, 'videoResolution').lower()
|
||||
video_full_resolution = ''
|
||||
if video_full_resolution_scan_type is not None:
|
||||
video_full_resolution = common.VIDEO_RESOLUTION_OVERRIDES.get(
|
||||
video_resolution, video_resolution + (video_full_resolution_scan_type[:1] or 'p')
|
||||
)
|
||||
video_resolution = helpers.get_xml_attr(media, 'videoResolution').lower().rstrip('ip')
|
||||
video_full_resolution = common.VIDEO_RESOLUTION_OVERRIDES.get(
|
||||
video_resolution, video_resolution + (video_full_resolution_scan_type[:1] or 'p')
|
||||
)
|
||||
|
||||
audio_channels = helpers.get_xml_attr(media, 'audioChannels')
|
||||
|
||||
medias.append({'id': helpers.get_xml_attr(media, 'id'),
|
||||
'container': helpers.get_xml_attr(media, 'container'),
|
||||
'bitrate': helpers.get_xml_attr(media, 'bitrate'),
|
||||
'height': helpers.get_xml_attr(media, 'height'),
|
||||
'width': helpers.get_xml_attr(media, 'width'),
|
||||
'aspect_ratio': helpers.get_xml_attr(media, 'aspectRatio'),
|
||||
'video_codec': helpers.get_xml_attr(media, 'videoCodec'),
|
||||
'video_resolution': video_resolution,
|
||||
'video_full_resolution': video_full_resolution,
|
||||
'video_framerate': helpers.get_xml_attr(media, 'videoFrameRate'),
|
||||
'video_profile': helpers.get_xml_attr(media, 'videoProfile'),
|
||||
'audio_codec': helpers.get_xml_attr(media, 'audioCodec'),
|
||||
'audio_channels': audio_channels,
|
||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||
'audio_profile': helpers.get_xml_attr(media, 'audioProfile'),
|
||||
'optimized_version': int(helpers.get_xml_attr(media, 'proxyType') == '42'),
|
||||
'parts': parts
|
||||
})
|
||||
media_info = {'id': helpers.get_xml_attr(media, 'id'),
|
||||
'container': helpers.get_xml_attr(media, 'container'),
|
||||
'bitrate': helpers.get_xml_attr(media, 'bitrate'),
|
||||
'height': helpers.get_xml_attr(media, 'height'),
|
||||
'width': helpers.get_xml_attr(media, 'width'),
|
||||
'aspect_ratio': helpers.get_xml_attr(media, 'aspectRatio'),
|
||||
'video_codec': helpers.get_xml_attr(media, 'videoCodec'),
|
||||
'video_resolution': video_resolution,
|
||||
'video_full_resolution': video_full_resolution,
|
||||
'video_framerate': helpers.get_xml_attr(media, 'videoFrameRate'),
|
||||
'video_profile': helpers.get_xml_attr(media, 'videoProfile'),
|
||||
'audio_codec': helpers.get_xml_attr(media, 'audioCodec'),
|
||||
'audio_channels': audio_channels,
|
||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||
'audio_profile': helpers.get_xml_attr(media, 'audioProfile'),
|
||||
'optimized_version': int(helpers.get_xml_attr(media, 'proxyType') == '42'),
|
||||
'channel_call_sign': helpers.get_xml_attr(media, 'channelCallSign'),
|
||||
'channel_identifier': helpers.get_xml_attr(media, 'channelIdentifier'),
|
||||
'channel_thumb': helpers.get_xml_attr(media, 'channelThumb'),
|
||||
'parts': parts
|
||||
}
|
||||
|
||||
video_full_resolution = helpers.get_xml_attr(media, 'videoResolution').lower()
|
||||
medias.append(media_info)
|
||||
|
||||
metadata['media_info'] = medias
|
||||
|
||||
@@ -1459,7 +1504,7 @@ class PmsConnect(object):
|
||||
|
||||
return metadata_list
|
||||
|
||||
def get_current_activity(self):
|
||||
def get_current_activity(self, skip_cache=False):
|
||||
"""
|
||||
Return processed and validated session list.
|
||||
|
||||
@@ -1486,17 +1531,17 @@ class PmsConnect(object):
|
||||
if a.getElementsByTagName('Track'):
|
||||
session_data = a.getElementsByTagName('Track')
|
||||
for session_ in session_data:
|
||||
session_output = self.get_session_each(session_)
|
||||
session_output = self.get_session_each(session_, skip_cache=skip_cache)
|
||||
session_list.append(session_output)
|
||||
if a.getElementsByTagName('Video'):
|
||||
session_data = a.getElementsByTagName('Video')
|
||||
for session_ in session_data:
|
||||
session_output = self.get_session_each(session_)
|
||||
session_output = self.get_session_each(session_, skip_cache=skip_cache)
|
||||
session_list.append(session_output)
|
||||
if a.getElementsByTagName('Photo'):
|
||||
session_data = a.getElementsByTagName('Photo')
|
||||
for session_ in session_data:
|
||||
session_output = self.get_session_each(session_)
|
||||
session_output = self.get_session_each(session_, skip_cache=skip_cache)
|
||||
session_list.append(session_output)
|
||||
|
||||
session_list = sorted(session_list, key=lambda k: k['session_key'])
|
||||
@@ -1507,7 +1552,7 @@ class PmsConnect(object):
|
||||
|
||||
return output
|
||||
|
||||
def get_session_each(self, session=None):
|
||||
def get_session_each(self, session=None, skip_cache=False):
|
||||
"""
|
||||
Return selected data from current sessions.
|
||||
This function processes and validates session data
|
||||
@@ -1698,6 +1743,11 @@ class PmsConnect(object):
|
||||
video_id = helpers.get_xml_attr(video_stream_info, 'id')
|
||||
video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'),
|
||||
'stream_video_bit_depth': helpers.get_xml_attr(video_stream_info, 'bitDepth'),
|
||||
'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info, 'chromaSubsampling'),
|
||||
'stream_video_color_primaries': helpers.get_xml_attr(video_stream_info, 'colorPrimaries'),
|
||||
'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'),
|
||||
'stream_video_color_space': helpers.get_xml_attr(video_stream_info, 'colorSpace'),
|
||||
'stream_video_color_trc': helpers.get_xml_attr(video_stream_info, 'colorTrc'),
|
||||
'stream_video_codec_level': helpers.get_xml_attr(video_stream_info, 'level'),
|
||||
'stream_video_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'),
|
||||
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
|
||||
@@ -1708,6 +1758,11 @@ class PmsConnect(object):
|
||||
else:
|
||||
video_details = {'stream_video_bitrate': '',
|
||||
'stream_video_bit_depth': '',
|
||||
'stream_video_chroma_subsampling': '',
|
||||
'stream_video_color_primaries': '',
|
||||
'stream_video_color_range': '',
|
||||
'stream_video_color_space': '',
|
||||
'stream_video_color_trc': '',
|
||||
'stream_video_codec_level': '',
|
||||
'stream_video_ref_frames': '',
|
||||
'stream_video_language': '',
|
||||
@@ -1773,7 +1828,7 @@ class PmsConnect(object):
|
||||
if helpers.cast_to_int(stream_video_width) >= 3840:
|
||||
stream_video_resolution = '4k'
|
||||
else:
|
||||
stream_video_resolution = helpers.get_xml_attr(stream_media_info, 'videoResolution').rstrip('p').lower()
|
||||
stream_video_resolution = helpers.get_xml_attr(stream_media_info, 'videoResolution').lower().rstrip('ip')
|
||||
|
||||
stream_audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels')
|
||||
|
||||
@@ -1850,13 +1905,19 @@ class PmsConnect(object):
|
||||
'full_title': helpers.get_xml_attr(session, 'title'),
|
||||
'container': helpers.get_xml_attr(stream_media_info, 'container') \
|
||||
or helpers.get_xml_attr(stream_media_parts_info, 'container'),
|
||||
'bitrate': helpers.get_xml_attr(stream_media_info, 'bitrate'),
|
||||
'height': helpers.get_xml_attr(stream_media_info, 'height'),
|
||||
'width': helpers.get_xml_attr(stream_media_info, 'width'),
|
||||
'aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'),
|
||||
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
|
||||
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(),
|
||||
'video_full_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(),
|
||||
'video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'),
|
||||
'video_profile': helpers.get_xml_attr(stream_media_info, 'videoProfile'),
|
||||
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
|
||||
'audio_channels': audio_channels,
|
||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||
'audio_profile': helpers.get_xml_attr(stream_media_info, 'audioProfile'),
|
||||
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
||||
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
|
||||
'extra_type': helpers.get_xml_attr(session, 'extraType'),
|
||||
@@ -1869,9 +1930,11 @@ class PmsConnect(object):
|
||||
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
|
||||
|
||||
if sync_id:
|
||||
metadata_details = self.get_metadata_details(rating_key=rating_key, sync_id=sync_id, cache_key=session_key)
|
||||
metadata_details = self.get_metadata_details(rating_key=rating_key, sync_id=sync_id,
|
||||
skip_cache=skip_cache, cache_key=session_key)
|
||||
else:
|
||||
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
|
||||
metadata_details = self.get_metadata_details(rating_key=rating_key,
|
||||
skip_cache=skip_cache, cache_key=session_key)
|
||||
|
||||
# Get the media info, fallback to first item if match id is not found
|
||||
source_medias = metadata_details.pop('media_info', [])
|
||||
@@ -1886,6 +1949,11 @@ class PmsConnect(object):
|
||||
'video_codec_level': '',
|
||||
'video_bitrate': '',
|
||||
'video_bit_depth': '',
|
||||
'video_chroma_subsampling': '',
|
||||
'video_color_primaries': '',
|
||||
'video_color_range': '',
|
||||
'video_color_space': '',
|
||||
'video_color_trc': '',
|
||||
'video_frame_rate': '',
|
||||
'video_ref_frames': '',
|
||||
'video_height': '',
|
||||
@@ -1953,15 +2021,15 @@ class PmsConnect(object):
|
||||
|
||||
# Override * in audio codecs
|
||||
if stream_details['stream_audio_codec'] == '*':
|
||||
stream_details['stream_audio_codec'] = source_audio_details['audio_codec']
|
||||
stream_details['stream_audio_codec'] = source_audio_details.get('audio_codec', '')
|
||||
if transcode_details['transcode_audio_codec'] == '*':
|
||||
transcode_details['transcode_audio_codec'] = source_audio_details['audio_codec']
|
||||
transcode_details['transcode_audio_codec'] = source_audio_details.get('audio_codec', '')
|
||||
|
||||
# Override * in video codecs
|
||||
if stream_details['stream_video_codec'] == '*':
|
||||
stream_details['stream_video_codec'] = source_video_details['video_codec']
|
||||
stream_details['stream_video_codec'] = source_video_details.get('video_codec', '')
|
||||
if transcode_details['transcode_video_codec'] == '*':
|
||||
transcode_details['transcode_video_codec'] = source_video_details['video_codec']
|
||||
transcode_details['transcode_video_codec'] = source_video_details.get('video_codec', '')
|
||||
|
||||
if media_type in ('movie', 'episode', 'clip'):
|
||||
# Set the full resolution by combining stream_video_resolution and stream_video_scan_type
|
||||
@@ -1969,6 +2037,23 @@ class PmsConnect(object):
|
||||
stream_details['stream_video_resolution'],
|
||||
stream_details['stream_video_resolution'] + (video_details['stream_video_scan_type'][:1] or 'p'))
|
||||
|
||||
if helpers.cast_to_int(source_video_details.get('video_bit_depth')) > 8 \
|
||||
and source_video_details.get('video_color_space') == 'bt2020nc':
|
||||
stream_details['video_dynamic_range'] = 'HDR'
|
||||
else:
|
||||
stream_details['video_dynamic_range'] = 'SDR'
|
||||
|
||||
if stream_details['video_dynamic_range'] == 'HDR' \
|
||||
and video_details['stream_video_decision'] != 'transcode' \
|
||||
or helpers.cast_to_int(video_details['stream_video_bit_depth']) > 8 \
|
||||
and video_details['stream_video_color_space'] == 'bt2020nc':
|
||||
stream_details['stream_video_dynamic_range'] = 'HDR'
|
||||
else:
|
||||
stream_details['stream_video_dynamic_range'] = 'SDR'
|
||||
else:
|
||||
stream_details['video_dynamic_range'] = ''
|
||||
stream_details['stream_video_dynamic_range'] = ''
|
||||
|
||||
# Get the quality profile
|
||||
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
||||
if sync_id:
|
||||
@@ -1995,7 +2080,7 @@ class PmsConnect(object):
|
||||
if stream_details['optimized_version']:
|
||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
||||
source_media_details['video_full_resolution'])
|
||||
source_media_details.get('video_full_resolution'))
|
||||
else:
|
||||
optimized_version_profile = ''
|
||||
|
||||
@@ -2280,7 +2365,7 @@ class PmsConnect(object):
|
||||
}
|
||||
children_results_list[media_type].append(children_output)
|
||||
|
||||
output = {'results_count': sum(len(s) for s in children_results_list.items()),
|
||||
output = {'results_count': sum(len(v) for k, v in children_results_list.items()),
|
||||
'results_list': children_results_list,
|
||||
}
|
||||
|
||||
@@ -2558,7 +2643,8 @@ class PmsConnect(object):
|
||||
'agent': library['agent'],
|
||||
'thumb': library['thumb'],
|
||||
'art': library['art'],
|
||||
'count': children_list['library_count']
|
||||
'count': children_list['library_count'],
|
||||
'is_active': 1
|
||||
}
|
||||
|
||||
if section_type == 'show':
|
||||
@@ -2644,10 +2730,14 @@ class PmsConnect(object):
|
||||
height = height or 1500
|
||||
|
||||
if img:
|
||||
if refresh:
|
||||
web_img = img.startswith('http')
|
||||
|
||||
if refresh and not web_img:
|
||||
img = '{}/{}'.format(img.rstrip('/'), int(time.time()))
|
||||
|
||||
if clip:
|
||||
if web_img:
|
||||
params = {'url': '%s' % img}
|
||||
elif clip:
|
||||
params = {'url': '%s&%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
|
||||
else:
|
||||
params = {'url': 'http://127.0.0.1:32400%s?%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
|
||||
|
225
plexpy/users.py
@@ -34,7 +34,11 @@ def refresh_users():
|
||||
if result:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
# Keep track of user_id to update is_active status
|
||||
user_ids = [0] # Local user always considered active
|
||||
|
||||
for item in result:
|
||||
user_ids.append(helpers.cast_to_int(item['user_id']))
|
||||
|
||||
if item.get('shared_libraries'):
|
||||
item['shared_libraries'] = ';'.join(item['shared_libraries'])
|
||||
@@ -58,6 +62,9 @@ def refresh_users():
|
||||
|
||||
monitor_db.upsert('users', item, keys_dict)
|
||||
|
||||
query = 'UPDATE users SET is_active = 0 WHERE user_id NOT IN ({})'.format(', '.join(['?'] * len(user_ids)))
|
||||
monitor_db.action(query=query, args=user_ids)
|
||||
|
||||
logger.info(u"Tautulli Users :: Users list refreshed.")
|
||||
return True
|
||||
else:
|
||||
@@ -92,7 +99,8 @@ class Users(object):
|
||||
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
|
||||
columns = ['users.user_id',
|
||||
columns = ['users.id AS row_id',
|
||||
'users.user_id',
|
||||
'(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" \
|
||||
THEN users.username ELSE users.friendly_name END) AS friendly_name',
|
||||
'users.thumb AS user_thumb',
|
||||
@@ -102,7 +110,7 @@ class Users(object):
|
||||
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
|
||||
session_history.paused_counter END) AS duration',
|
||||
'MAX(session_history.started) AS last_seen',
|
||||
'MAX(session_history.id) AS id',
|
||||
'MAX(session_history.id) AS history_row_id',
|
||||
'session_history_metadata.full_title AS last_played',
|
||||
'session_history.ip_address',
|
||||
'session_history.platform',
|
||||
@@ -116,10 +124,15 @@ class Users(object):
|
||||
'session_history_metadata.year',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_metadata.live',
|
||||
'session_history_metadata.added_at',
|
||||
'session_history_metadata.originally_available_at',
|
||||
'session_history_metadata.guid',
|
||||
'session_history_media_info.transcode_decision',
|
||||
'users.do_notify as do_notify',
|
||||
'users.keep_history as keep_history',
|
||||
'users.allow_guest as allow_guest'
|
||||
'users.do_notify AS do_notify',
|
||||
'users.keep_history AS keep_history',
|
||||
'users.allow_guest AS allow_guest',
|
||||
'users.is_active AS is_active'
|
||||
]
|
||||
try:
|
||||
query = data_tables.ssp_query(table_name='users',
|
||||
@@ -161,14 +174,15 @@ class Users(object):
|
||||
# Rename Mystery platform names
|
||||
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
||||
|
||||
row = {'user_id': item['user_id'],
|
||||
row = {'row_id': item['row_id'],
|
||||
'user_id': item['user_id'],
|
||||
'friendly_name': item['friendly_name'],
|
||||
'user_thumb': user_thumb,
|
||||
'plays': item['plays'],
|
||||
'duration': item['duration'],
|
||||
'last_seen': item['last_seen'],
|
||||
'last_played': item['last_played'],
|
||||
'id': item['id'],
|
||||
'history_row_id': item['history_row_id'],
|
||||
'ip_address': item['ip_address'],
|
||||
'platform': platform,
|
||||
'player': item['player'],
|
||||
@@ -179,10 +193,14 @@ class Users(object):
|
||||
'year': item['year'],
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'live': item['live'],
|
||||
'originally_available_at': item['originally_available_at'],
|
||||
'guid': item['guid'],
|
||||
'transcode_decision': item['transcode_decision'],
|
||||
'do_notify': helpers.checked(item['do_notify']),
|
||||
'keep_history': helpers.checked(item['keep_history']),
|
||||
'allow_guest': helpers.checked(item['allow_guest'])
|
||||
'allow_guest': helpers.checked(item['allow_guest']),
|
||||
'is_active': item['is_active']
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
@@ -209,7 +227,7 @@ class Users(object):
|
||||
|
||||
custom_where = ['users.user_id', user_id]
|
||||
|
||||
columns = ['session_history.id',
|
||||
columns = ['session_history.id AS history_row_id',
|
||||
'MAX(session_history.started) AS last_seen',
|
||||
'session_history.ip_address',
|
||||
'COUNT(session_history.id) AS play_count',
|
||||
@@ -225,6 +243,10 @@ class Users(object):
|
||||
'session_history_metadata.year',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_metadata.live',
|
||||
'session_history_metadata.added_at',
|
||||
'session_history_metadata.originally_available_at',
|
||||
'session_history_metadata.guid',
|
||||
'session_history_media_info.transcode_decision',
|
||||
'session_history.user',
|
||||
'session_history.user_id as custom_user_id',
|
||||
@@ -265,7 +287,7 @@ class Users(object):
|
||||
# Rename Mystery platform names
|
||||
platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
|
||||
|
||||
row = {'id': item['id'],
|
||||
row = {'history_row_id': item['history_row_id'],
|
||||
'last_seen': item['last_seen'],
|
||||
'ip_address': item['ip_address'],
|
||||
'play_count': item['play_count'],
|
||||
@@ -279,6 +301,9 @@ class Users(object):
|
||||
'year': item['year'],
|
||||
'media_index': item['media_index'],
|
||||
'parent_media_index': item['parent_media_index'],
|
||||
'live': item['live'],
|
||||
'originally_available_at': item['originally_available_at'],
|
||||
'guid': item['guid'],
|
||||
'transcode_decision': item['transcode_decision'],
|
||||
'friendly_name': item['friendly_name'],
|
||||
'user_id': item['custom_user_id']
|
||||
@@ -311,11 +336,13 @@ class Users(object):
|
||||
logger.warn(u"Tautulli Users :: Unable to execute database query for set_config: %s." % e)
|
||||
|
||||
def get_details(self, user_id=None, user=None, email=None):
|
||||
default_return = {'user_id': 0,
|
||||
default_return = {'row_id': 0,
|
||||
'user_id': 0,
|
||||
'username': 'Local',
|
||||
'friendly_name': 'Local',
|
||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||
'email': '',
|
||||
'is_active': 1,
|
||||
'is_admin': '',
|
||||
'is_home_user': 0,
|
||||
'is_allow_sync': 0,
|
||||
@@ -335,22 +362,28 @@ class Users(object):
|
||||
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_admin, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
||||
query = 'SELECT id AS row_id, user_id, username, friendly_name, ' \
|
||||
'thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \
|
||||
'do_notify, keep_history, deleted_user, ' \
|
||||
'allow_guest, shared_libraries ' \
|
||||
'FROM users ' \
|
||||
'WHERE user_id = ? '
|
||||
result = monitor_db.select(query, args=[user_id])
|
||||
elif user:
|
||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_admin, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
||||
query = 'SELECT id AS row_id, user_id, username, friendly_name, ' \
|
||||
'thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \
|
||||
'do_notify, keep_history, deleted_user, ' \
|
||||
'allow_guest, shared_libraries ' \
|
||||
'FROM users ' \
|
||||
'WHERE username = ? COLLATE NOCASE '
|
||||
result = monitor_db.select(query, args=[user])
|
||||
elif email:
|
||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_admin, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
||||
query = 'SELECT id AS row_id, user_id, username, friendly_name, ' \
|
||||
'thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \
|
||||
'do_notify, keep_history, deleted_user, ' \
|
||||
'allow_guest, shared_libraries ' \
|
||||
'FROM users ' \
|
||||
'WHERE email = ? COLLATE NOCASE '
|
||||
@@ -380,11 +413,13 @@ class Users(object):
|
||||
|
||||
shared_libraries = tuple(item['shared_libraries'].split(';')) if item['shared_libraries'] else ()
|
||||
|
||||
user_details = {'user_id': item['user_id'],
|
||||
user_details = {'row_id': item['row_id'],
|
||||
'user_id': item['user_id'],
|
||||
'username': item['username'],
|
||||
'friendly_name': friendly_name,
|
||||
'user_thumb': user_thumb,
|
||||
'email': item['email'],
|
||||
'is_active': item['is_active'],
|
||||
'is_admin': item['is_admin'],
|
||||
'is_home_user': item['is_home_user'],
|
||||
'is_allow_sync': item['is_allow_sync'],
|
||||
@@ -420,21 +455,25 @@ class Users(object):
|
||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||
return default_return
|
||||
|
||||
def get_watch_time_stats(self, user_id=None, grouping=None):
|
||||
def get_watch_time_stats(self, user_id=None, grouping=None, query_days=None):
|
||||
if not session.allow_session_user(user_id):
|
||||
return []
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
if query_days and query_days is not None:
|
||||
query_days = map(helpers.cast_to_int, query_days.split(','))
|
||||
else:
|
||||
query_days = [1, 7, 30, 0]
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
time_queries = [1, 7, 30, 0]
|
||||
user_watch_time_stats = []
|
||||
|
||||
group_by = 'reference_id' if grouping else 'id'
|
||||
|
||||
for days in time_queries:
|
||||
for days in query_days:
|
||||
try:
|
||||
if days > 0:
|
||||
if str(user_id).isdigit():
|
||||
@@ -534,11 +573,11 @@ class Users(object):
|
||||
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
query = 'SELECT session_history.id, session_history.media_type, ' \
|
||||
query = 'SELECT session_history.id, session_history.media_type, guid, ' \
|
||||
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
|
||||
'title, parent_title, grandparent_title, original_title, ' \
|
||||
'thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
|
||||
'year, started, user ' \
|
||||
'year, originally_available_at, added_at, live, started, user ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE user_id = ? ' \
|
||||
@@ -553,30 +592,33 @@ class Users(object):
|
||||
result = []
|
||||
|
||||
for row in result:
|
||||
if row['media_type'] == 'episode' and row['parent_thumb']:
|
||||
thumb = row['parent_thumb']
|
||||
elif row['media_type'] == 'episode':
|
||||
thumb = row['grandparent_thumb']
|
||||
else:
|
||||
thumb = row['thumb']
|
||||
if row['media_type'] == 'episode' and row['parent_thumb']:
|
||||
thumb = row['parent_thumb']
|
||||
elif row['media_type'] == 'episode':
|
||||
thumb = row['grandparent_thumb']
|
||||
else:
|
||||
thumb = row['thumb']
|
||||
|
||||
recent_output = {'row_id': row['id'],
|
||||
'media_type': row['media_type'],
|
||||
'rating_key': row['rating_key'],
|
||||
'parent_rating_key': row['parent_rating_key'],
|
||||
'grandparent_rating_key': row['grandparent_rating_key'],
|
||||
'title': row['title'],
|
||||
'parent_title': row['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': row['original_title'],
|
||||
'thumb': thumb,
|
||||
'media_index': row['media_index'],
|
||||
'parent_media_index': row['parent_media_index'],
|
||||
'year': row['year'],
|
||||
'time': row['started'],
|
||||
'user': row['user']
|
||||
}
|
||||
recently_watched.append(recent_output)
|
||||
recent_output = {'row_id': row['id'],
|
||||
'media_type': row['media_type'],
|
||||
'rating_key': row['rating_key'],
|
||||
'parent_rating_key': row['parent_rating_key'],
|
||||
'grandparent_rating_key': row['grandparent_rating_key'],
|
||||
'title': row['title'],
|
||||
'parent_title': row['parent_title'],
|
||||
'grandparent_title': row['grandparent_title'],
|
||||
'original_title': row['original_title'],
|
||||
'thumb': thumb,
|
||||
'media_index': row['media_index'],
|
||||
'parent_media_index': row['parent_media_index'],
|
||||
'year': row['year'],
|
||||
'originally_available_at': row['originally_available_at'],
|
||||
'live': row['live'],
|
||||
'guid': row['guid'],
|
||||
'time': row['started'],
|
||||
'user': row['user']
|
||||
}
|
||||
recently_watched.append(recent_output)
|
||||
|
||||
return recently_watched
|
||||
|
||||
@@ -584,8 +626,8 @@ class Users(object):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
query = 'SELECT user_id, username, friendly_name, thumb, custom_avatar_url, email, ' \
|
||||
'is_admin, is_home_user, is_allow_sync, is_restricted, ' \
|
||||
query = 'SELECT id AS row_id, user_id, username, friendly_name, thumb, custom_avatar_url, email, ' \
|
||||
'is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \
|
||||
'do_notify, keep_history, allow_guest, server_token, shared_libraries, ' \
|
||||
'filter_all, filter_movies, filter_tv, filter_music, filter_photos ' \
|
||||
'FROM users WHERE deleted_user = 0'
|
||||
@@ -596,11 +638,13 @@ class Users(object):
|
||||
|
||||
users = []
|
||||
for item in result:
|
||||
user = {'user_id': item['user_id'],
|
||||
user = {'row_id': item['row_id'],
|
||||
'user_id': item['user_id'],
|
||||
'username': item['username'],
|
||||
'friendly_name': item['friendly_name'] or item['username'],
|
||||
'thumb': item['custom_avatar_url'] or item['thumb'],
|
||||
'email': item['email'],
|
||||
'is_active': item['is_active'],
|
||||
'is_admin': item['is_admin'],
|
||||
'is_home_user': item['is_home_user'],
|
||||
'is_allow_sync': item['is_allow_sync'],
|
||||
@@ -620,53 +664,40 @@ class Users(object):
|
||||
|
||||
return users
|
||||
|
||||
def delete_all_history(self, user_id=None):
|
||||
def delete(self, user_id=None, row_ids=None, purge_only=False):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
logger.info(u"Tautulli Users :: Deleting all history for user id %s from database." % user_id)
|
||||
session_history_media_info_del = \
|
||||
monitor_db.action('DELETE FROM '
|
||||
'session_history_media_info '
|
||||
'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id '
|
||||
'FROM session_history_media_info '
|
||||
'JOIN session_history ON session_history_media_info.id = session_history.id '
|
||||
'WHERE session_history.user_id = ?)', [user_id])
|
||||
session_history_metadata_del = \
|
||||
monitor_db.action('DELETE FROM '
|
||||
'session_history_metadata '
|
||||
'WHERE session_history_metadata.id IN (SELECT session_history_metadata.id '
|
||||
'FROM session_history_metadata '
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id '
|
||||
'WHERE session_history.user_id = ?)', [user_id])
|
||||
session_history_del = \
|
||||
monitor_db.action('DELETE FROM '
|
||||
'session_history '
|
||||
'WHERE session_history.user_id = ?', [user_id])
|
||||
if row_ids and row_ids is not None:
|
||||
row_ids = map(helpers.cast_to_int, row_ids.split(','))
|
||||
|
||||
return 'Deleted all items for user_id %s.' % user_id
|
||||
# Get the user_ids corresponding to the row_ids
|
||||
result = monitor_db.select('SELECT user_id FROM users '
|
||||
'WHERE id IN ({})'.format(','.join(['?'] * len(row_ids))), row_ids)
|
||||
|
||||
success = []
|
||||
for user in result:
|
||||
success.append(self.delete(user_id=user['user_id'],
|
||||
purge_only=purge_only))
|
||||
return all(success)
|
||||
|
||||
elif str(user_id).isdigit():
|
||||
delete_success = database.delete_user_history(user_id=user_id)
|
||||
|
||||
if purge_only:
|
||||
return delete_success
|
||||
else:
|
||||
return 'Unable to delete items. Input user_id not valid.'
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Users :: Unable to execute database query for delete_all_history: %s." % e)
|
||||
logger.info(u"Tautulli Users :: Deleting user with user_id %s from database."
|
||||
% user_id)
|
||||
try:
|
||||
monitor_db.action('UPDATE users '
|
||||
'SET deleted_user = 1, keep_history = 0, do_notify = 0 '
|
||||
'WHERE user_id = ?', [user_id])
|
||||
return delete_success
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Users :: Unable to execute database query for delete: %s." % e)
|
||||
|
||||
def delete(self, user_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
self.delete_all_history(user_id)
|
||||
logger.info(u"Tautulli Users :: Deleting user with id %s from database." % user_id)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 1 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET keep_history = 0 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET do_notify = 0 WHERE user_id = ?', [user_id])
|
||||
|
||||
return 'Deleted user with id %s.' % user_id
|
||||
else:
|
||||
return 'Unable to delete user, user_id not valid.'
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Users :: Unable to execute database query for delete: %s." % e)
|
||||
else:
|
||||
return False
|
||||
|
||||
def undelete(self, user_id=None, username=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
@@ -677,9 +708,9 @@ class Users(object):
|
||||
result = monitor_db.select(query=query, args=[user_id])
|
||||
if result:
|
||||
logger.info(u"Tautulli Users :: Re-adding user with id %s to database." % user_id)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET keep_history = 1 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET do_notify = 1 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users '
|
||||
'SET deleted_user = 0, keep_history = 1, do_notify = 1 '
|
||||
'WHERE user_id = ?', [user_id])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -689,9 +720,9 @@ class Users(object):
|
||||
result = monitor_db.select(query=query, args=[username])
|
||||
if result:
|
||||
logger.info(u"Tautulli Users :: Re-adding user with username %s to database." % username)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users SET keep_history = 1 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users SET do_notify = 1 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users '
|
||||
'SET deleted_user = 0, keep_history = 1, do_notify = 1 '
|
||||
'WHERE username = ?', [username])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.41"
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.2.2-beta"
|
||||
|
@@ -115,20 +115,24 @@ def getVersion():
|
||||
|
||||
else:
|
||||
|
||||
plexpy.INSTALL_TYPE = 'source'
|
||||
plexpy.INSTALL_TYPE = 'docker' if plexpy.DOCKER else 'source'
|
||||
|
||||
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
||||
branch_file = os.path.join(plexpy.PROG_DIR, 'branch.txt')
|
||||
|
||||
if not os.path.isfile(version_file):
|
||||
return None, 'origin', common.BRANCH
|
||||
|
||||
with open(version_file, 'r') as f:
|
||||
current_version = f.read().strip(' \n\r')
|
||||
|
||||
if current_version:
|
||||
return current_version, 'origin', common.BRANCH
|
||||
if os.path.isfile(version_file):
|
||||
with open(version_file, 'r') as f:
|
||||
current_version = f.read().strip(' \n\r')
|
||||
else:
|
||||
return None, 'origin', common.BRANCH
|
||||
current_version = None
|
||||
|
||||
if os.path.isfile(branch_file):
|
||||
with open(branch_file, 'r') as f:
|
||||
current_branch = f.read().strip(' \n\r')
|
||||
else:
|
||||
current_branch = common.BRANCH
|
||||
|
||||
return current_version, 'origin', current_branch
|
||||
|
||||
|
||||
def check_update(auto_update=False, notify=False):
|
||||
@@ -158,13 +162,17 @@ def check_update(auto_update=False, notify=False):
|
||||
def check_github(auto_update=False, notify=False):
|
||||
plexpy.COMMITS_BEHIND = 0
|
||||
|
||||
if plexpy.CONFIG.GIT_TOKEN:
|
||||
headers = {'Authorization': 'token {}'.format(plexpy.CONFIG.GIT_TOKEN)}
|
||||
else:
|
||||
headers = {}
|
||||
|
||||
# Get the latest version available from github
|
||||
logger.info('Retrieving latest version information from GitHub')
|
||||
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.CONFIG.GIT_BRANCH)
|
||||
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
|
||||
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
|
||||
version = request.request_json(url, headers=headers, timeout=20, validator=lambda x: type(x) == dict)
|
||||
|
||||
if version is None:
|
||||
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
@@ -187,8 +195,8 @@ def check_github(auto_update=False, notify=False):
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.LATEST_VERSION,
|
||||
plexpy.CURRENT_VERSION)
|
||||
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
|
||||
validator=lambda x: type(x) == dict)
|
||||
|
||||
if commits is None:
|
||||
logger.warn('Could not get commits behind from GitHub.')
|
||||
@@ -228,7 +236,7 @@ def check_github(auto_update=False, notify=False):
|
||||
'plexpy_update_commit': plexpy.LATEST_VERSION,
|
||||
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
||||
|
||||
if auto_update:
|
||||
if auto_update and not plexpy.DOCKER:
|
||||
logger.info('Running automatic update.')
|
||||
plexpy.shutdown(restart=True, update=True)
|
||||
|
||||
@@ -243,23 +251,26 @@ def update():
|
||||
logger.info('Windows .exe updating not supported yet.')
|
||||
|
||||
elif plexpy.INSTALL_TYPE == 'git':
|
||||
output, err = runGit('pull ' + plexpy.CONFIG.GIT_REMOTE + ' ' + plexpy.CONFIG.GIT_BRANCH)
|
||||
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_BRANCH))
|
||||
|
||||
if not output:
|
||||
logger.error('Unable to download latest version')
|
||||
return
|
||||
|
||||
for line in output.split('\n'):
|
||||
|
||||
if 'Already up-to-date.' in line:
|
||||
if 'Already up-to-date.' in line or 'Already up to date.' in line:
|
||||
logger.info('No update available, not updating')
|
||||
logger.info('Output: ' + str(output))
|
||||
elif line.endswith(('Aborting', 'Aborting.')):
|
||||
logger.error('Unable to update from git: ' + line)
|
||||
logger.info('Output: ' + str(output))
|
||||
|
||||
elif plexpy.INSTALL_TYPE == 'docker':
|
||||
return
|
||||
|
||||
else:
|
||||
tar_download_url = 'https://github.com/{}/{}/tarball/{}'.format(plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH)
|
||||
tar_download_url = 'https://github.com/{}/{}/tarball/{}'.format(plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.CONFIG.GIT_BRANCH)
|
||||
update_dir = os.path.join(plexpy.PROG_DIR, 'update')
|
||||
version_path = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
||||
|
||||
@@ -317,10 +328,41 @@ def update():
|
||||
return
|
||||
|
||||
|
||||
def reset_git_install():
|
||||
if plexpy.INSTALL_TYPE == 'git':
|
||||
logger.info('Attempting to reset git install to "{}/{}/{}"'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_BRANCH,
|
||||
common.RELEASE))
|
||||
|
||||
output, err = runGit('remote set-url {} https://github.com/{}/{}.git'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO))
|
||||
output, err = runGit('fetch {}'.format(plexpy.CONFIG.GIT_REMOTE))
|
||||
output, err = runGit('checkout {}'.format(plexpy.CONFIG.GIT_BRANCH))
|
||||
output, err = runGit('branch -u {}/{}'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_BRANCH))
|
||||
output, err = runGit('reset --hard {}'.format(common.RELEASE))
|
||||
|
||||
if not output:
|
||||
logger.error('Unable to reset Tautulli installation.')
|
||||
return False
|
||||
|
||||
for line in output.split('\n'):
|
||||
if 'Already up-to-date.' in line or 'Already up to date.' in line:
|
||||
logger.info('Tautulli installation reset successfully.')
|
||||
return True
|
||||
elif line.endswith(('Aborting', 'Aborting.')):
|
||||
logger.error('Unable to reset Tautulli installation: ' + line)
|
||||
return False
|
||||
|
||||
|
||||
def checkout_git_branch():
|
||||
if plexpy.INSTALL_TYPE == 'git':
|
||||
output, err = runGit('fetch %s' % plexpy.CONFIG.GIT_REMOTE)
|
||||
output, err = runGit('checkout %s' % plexpy.CONFIG.GIT_BRANCH)
|
||||
logger.info('Attempting to checkout git branch "{}/{}"'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_BRANCH))
|
||||
|
||||
output, err = runGit('fetch {}'.format(plexpy.CONFIG.GIT_REMOTE))
|
||||
output, err = runGit('checkout {}'.format(plexpy.CONFIG.GIT_BRANCH))
|
||||
|
||||
if not output:
|
||||
logger.error('Unable to change git branch.')
|
||||
@@ -329,9 +371,10 @@ def checkout_git_branch():
|
||||
for line in output.split('\n'):
|
||||
if line.endswith(('Aborting', 'Aborting.')):
|
||||
logger.error('Unable to checkout from git: ' + line)
|
||||
logger.info('Output: ' + str(output))
|
||||
return
|
||||
|
||||
output, err = runGit('pull %s %s' % (plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH))
|
||||
output, err = runGit('pull {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_BRANCH))
|
||||
|
||||
|
||||
def read_changelog(latest_only=False, since_prev_release=False):
|
||||
@@ -349,6 +392,9 @@ def read_changelog(latest_only=False, since_prev_release=False):
|
||||
header_pattern = re.compile(r'(^#+)\s(.+)')
|
||||
list_pattern = re.compile(r'(^[ \t]*\*\s)(.+)')
|
||||
|
||||
beta_release = False
|
||||
prev_release = str(plexpy.PREV_RELEASE)
|
||||
|
||||
with open(changelog_file, "r") as logfile:
|
||||
for line in logfile:
|
||||
line_header_match = re.search(header_pattern, line)
|
||||
@@ -366,8 +412,16 @@ def read_changelog(latest_only=False, since_prev_release=False):
|
||||
elif latest_only:
|
||||
latest_version_found = True
|
||||
# Add a space to the end of the release to match tags
|
||||
elif since_prev_release and str(plexpy.PREV_RELEASE) + ' ' in header_text:
|
||||
break
|
||||
elif since_prev_release:
|
||||
if prev_release.endswith('-beta') and not beta_release:
|
||||
if prev_release + ' ' in header_text:
|
||||
break
|
||||
elif prev_release.replace('-beta', '') + ' ' in header_text:
|
||||
beta_release = True
|
||||
elif prev_release.endswith('-beta') and beta_release:
|
||||
break
|
||||
elif prev_release + ' ' in header_text:
|
||||
break
|
||||
|
||||
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
||||
|
||||
|