Compare commits

..

70 Commits

Author SHA1 Message Date
JonnyWong16
6d5c320701 v2.2.2-beta 2020-04-12 21:27:01 -07:00
JonnyWong16
400a189455 Invalidate temporary mobile app token after 5 minutes 2020-04-12 21:20:14 -07:00
JonnyWong16
b7d03a4f31 Fix refreshing libraries and users table after deleting 2020-04-12 20:51:22 -07:00
JonnyWong16
523e6421be Don't delete library history if server_id doesn't match 2020-04-12 20:49:45 -07:00
JonnyWong16
e0cd6f7071 Rename docker build arg VERSION to TAG 2020-04-12 18:18:08 -07:00
JonnyWong16
38db0b7a70 Rename VERSION to COMMIT in Dockerfile 2020-04-12 18:15:14 -07:00
JonnyWong16
f39ecd89a7 Docker docker build badges on README 2020-04-12 18:03:40 -07:00
JonnyWong16
f7f76d82b6 Add Docker buildx GitHub workflow 2020-04-12 17:56:15 -07:00
JonnyWong16
9097e79e4f Change web app manifest start url to relative path 2020-04-12 11:33:23 -07:00
JonnyWong16
88711e7601 Add more info to manifest.json 2020-04-12 10:43:30 -07:00
JonnyWong16
d0fa83bb8c Use Kodi platform image for xbmc (Fixes Tautulli/Tautulli-Issues##231) 2020-04-11 20:39:39 -07:00
JonnyWong16
1271458f83 Fix web app manifest file (Fixes Tautulli/Tautulli-Issues#232) 2020-04-11 12:40:51 -07:00
JonnyWong16
2ae09a07e6 Some css syntax fixes 2020-04-11 12:39:12 -07:00
JonnyWong16
33d860384c Add Tautulli database corruption notification trigger 2020-04-10 15:11:32 -07:00
JonnyWong16
a4eda99a4a Update API docs to mention enabling api_sql while Tautulli is shut down 2020-04-10 14:44:14 -07:00
JonnyWong16
752c7badd2 Make inactive icon larger on library/user page 2020-04-10 14:41:14 -07:00
JonnyWong16
6399c90642 Fix platform icon size on activity card 2020-04-10 14:36:51 -07:00
JonnyWong16
97089846e9 Make inactive user/library triangle always orange 2020-04-10 14:31:12 -07:00
JonnyWong16
4de7884e39 Update API docs with all delete function changes 2020-04-10 14:14:34 -07:00
JonnyWong16
440adfb914 Fix missing page functions in library table 2020-04-10 14:12:38 -07:00
JonnyWong16
5f26d0085d Simplify library undelete function 2020-04-10 14:07:15 -07:00
JonnyWong16
f484604c69 Simplify user undelete function 2020-04-10 14:07:03 -07:00
JonnyWong16
899d2fbf9d Make library delete server_id optional 2020-04-10 14:03:32 -07:00
JonnyWong16
6a87dc9c40 Improve library delete/purge function 2020-04-10 14:00:19 -07:00
JonnyWong16
104e2929df Simplify user delete loop 2020-04-10 13:27:26 -07:00
JonnyWong16
faac6b11c2 Improve user delete/purge function 2020-04-10 13:15:29 -07:00
JonnyWong16
377a23478e Rename history id to row_id 2020-04-10 12:56:50 -07:00
JonnyWong16
c979e78802 Refactor database delete_session_history_rows ids 2020-04-10 12:40:35 -07:00
JonnyWong16
38f64c7d85 Improve delete history using list of row ids 2020-04-10 11:45:20 -07:00
JonnyWong16
1091a64863 Update API docs with library is_active 2020-04-10 00:20:25 -07:00
JonnyWong16
23de9616f1 Show library active status on Libraries table 2020-04-10 00:17:52 -07:00
JonnyWong16
198e7767dc Add library is_active to database 2020-04-10 00:12:38 -07:00
JonnyWong16
aa31bf1a19 Update API docs with user is_active 2020-04-10 00:11:43 -07:00
JonnyWong16
f366304c50 Show user active status on Users table 2020-04-10 00:02:30 -07:00
JonnyWong16
ce289995ff Add user is_active to database 2020-04-09 23:15:08 -07:00
JonnyWong16
ca2b4085c9 Fix if bad query_days passed to API 2020-04-09 18:33:02 -07:00
JonnyWong16
1d08069162 Rename time_queries to query_days 2020-04-09 18:30:46 -07:00
JonnyWong16
bcbfaae630 Merge pull request #1372 from KaasKop97/custom_time_queries
Allow custom time_queries for get_watch_stats (Closes #1345)
2020-04-09 18:21:35 -07:00
JonnyWong16
ae9df92d28 Divide file size by 2^10 but display SI units 2020-04-08 22:55:56 -07:00
JonnyWong16
47610323b0 Fix API grouping parameter not defaulting to match setting 2020-04-07 18:18:16 -07:00
Mitch
d1f1763919 Allow custom time_queries for get_watch_stats 2020-04-05 20:03:57 +02:00
JonnyWong16
1326ad8708 Add TAUTULLI_PYTHON_VERSION to script environment variables
* Period separated string (e.g. 2.7.17 or 3.8.2)
2020-04-04 08:12:42 -07:00
JonnyWong16
6e09e509bd Remove duplicate dictionary key in top movie stats 2020-04-03 21:07:00 -07:00
JonnyWong16
e8d0557852 Fix typo in send newsletter argument 2020-04-03 21:06:10 -07:00
JonnyWong16
aac705f465 Put import OpenSSL in try/except block for self-signed certificates 2020-04-03 21:05:44 -07:00
JonnyWong16
009971901b Fix delete lookup info by rating key 2020-04-03 20:53:35 -07:00
JonnyWong16
1ffd6c0ea1 Encode API XML output to UTF-8 2020-03-30 13:55:17 -07:00
JonnyWong16
50ce29cc64 Fix enable notification grouping by default again 2020-03-29 21:14:17 -07:00
JonnyWong16
e4ec24be26 Reorder git pull command for update 2020-03-29 10:28:42 -07:00
JonnyWong16
04765288d7 Change default file size on media info tables to SI units 2020-03-29 10:27:56 -07:00
JonnyWong16
8fdd0ba0d9 Merge pull request #1363 from aaronldunlap/master
Change humanFileSize to default to SI notation
2020-03-29 10:18:14 -07:00
JonnyWong16
16ffbd9940 v2.2.1 2020-03-28 15:11:02 -07:00
JonnyWong16
fa61302954 Fix saving mobile device with blank friendly name 2020-03-26 10:11:33 -07:00
JonnyWong16
763e5f583a Fix Windows system tray icon not enabled by default 2020-03-24 21:24:31 -07:00
JonnyWong16
395fc49087 Add ability to flush recently_added database table 2020-03-23 17:49:51 -07:00
JonnyWong16
d54794e85f Add favicon to newsletter template 2020-03-23 15:21:05 -07:00
JonnyWong16
d5917f89f0 Fix notification grouping not enabled by default on new install 2020-03-23 10:28:01 -07:00
JonnyWong16
1003aa2df5 Fix related children count 2020-03-21 18:34:04 -07:00
JonnyWong16
6205af1a9a Fix missing username on sync table 2020-03-17 09:44:10 -07:00
JonnyWong16
d8b1db536c Fix file size notification parameter truncated to integer (Fixes Tautulli/Tautlli-Issues#226) 2020-03-17 09:37:10 -07:00
JonnyWong16
699357ca21 Add transcode decision counts to notification parameters 2020-03-14 15:39:22 -07:00
JonnyWong16
50398049f5 Don't strip tags from webhook agent 2020-03-14 15:31:18 -07:00
JonnyWong16
1f83afc2f4 Update API docs for delete_lookup_info 2020-03-14 14:00:30 -07:00
JonnyWong16
90374bb46f Add buttons to delete all 3rd party metadata lookup info in the settings 2020-03-14 14:00:17 -07:00
JonnyWong16
ccdd410eda Change breadcrumbs on update metadata page to match info page 2020-03-14 13:58:01 -07:00
JonnyWong16
77bb806a01 Add IMDb and Rotten Tomatos rating to info page 2020-03-14 12:07:21 -07:00
JonnyWong16
a952352e1f Get metadata for the info page from the Plex server before database 2020-03-14 12:05:47 -07:00
JonnyWong16
b733ce969a Fix update changelog from beta 2020-03-08 17:03:21 -07:00
JonnyWong16
f4351df302 Combine release workflows 2020-03-08 16:37:19 -07:00
aaronldunlap
aa5affe366 Change humanFileSize to default to SI notation 2020-01-23 17:09:39 -06:00
56 changed files with 1138 additions and 643 deletions

View File

@@ -1,5 +1,8 @@
.git
.github
.gitignore
contrib
init-scripts
pylintrc
*.md
!CHANGELOG*.md

83
.github/workflows/publish-docker.yml vendored Normal file
View 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

View File

@@ -1,8 +1,7 @@
name: Create Pre-Release
name: Publish Release
on:
push:
tags:
- 'v*-beta'
tags: [v*]
jobs:
build:
runs-on: ubuntu-latest
@@ -10,7 +9,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Release Version
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF/refs\/tags\//}
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
@@ -26,4 +25,4 @@ jobs:
##${{ env.CHANGELOG }}
draft: false
prerelease: true
prerelease: ${{ endsWith(env.RELEASE_VERSION, '-beta') }}

View File

@@ -1,30 +0,0 @@
name: Publish Docker Branch
on:
push:
branches: [master, beta, nightly]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Branch
run: echo ::set-env name=BRANCH::${GITHUB_REF#refs/heads/}
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
env:
VERSION: ${{ github.sha }}
with:
name: tautulli/tautulli
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
buildargs: VERSION, BRANCH
- 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

View File

@@ -1,32 +0,0 @@
name: Publish Docker Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Branch
run: echo ::set-env name=BRANCH::${GITHUB_REF/refs\/tags\//}
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
env:
VERSION: ${{ github.sha }}
with:
name: tautulli/tautulli
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
buildargs: VERSION, BRANCH
tags: ${{ env.BRANCH }}
- 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

View File

@@ -1,30 +0,0 @@
name: Create Release
on:
push:
tags:
- 'v*'
- '!v*-beta'
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: false

62
API.md
View File

@@ -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
@@ -719,7 +743,6 @@ Returns:
"group_count": 1,
"group_ids": "1124",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1124,
"ip_address": "xxx.xxx.xxx.xxx",
"live": 0,
"media_index": 17,
@@ -735,6 +758,7 @@ Returns:
"player": "Castle-PC",
"rating_key": 4348,
"reference_id": 1123,
"row_id": 1124,
"session_key": null,
"started": 1462688107,
"state": null,
@@ -846,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",
@@ -887,7 +912,8 @@ Returns:
"do_notify_created": "Checked",
"duration": 1578037,
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1128,
"histroy_row_id": 1128,
"is_active": 1,
"keep_history": "Checked",
"labels": [],
"last_accessed": 1462693216,
@@ -903,9 +929,11 @@ Returns:
"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
},
@@ -933,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"
}
```
@@ -1059,6 +1090,7 @@ Required parameters:
Optional parameters:
grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns:
json:
@@ -2215,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",
@@ -2371,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:
@@ -2414,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",
@@ -2458,8 +2496,9 @@ Returns:
"duration": 2998290,
"friendly_name": "Jon Snow",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1121,
"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,
@@ -2473,6 +2512,7 @@ Returns:
"player": "Plex Web (Chrome)",
"plays": 487,
"rating_key": 153037,
"row_id": 1,
"thumb": "/library/metadata/153036/thumb/1462175062",
"transcode_decision": "transcode",
"user_id": 133788,
@@ -2734,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:

View File

@@ -1,5 +1,54 @@
# 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!

View File

@@ -1,9 +1,9 @@
FROM python:2.7.17-slim
FROM tautulli/tautulli-baseimage:latest
LABEL maintainer="TheMeanCanEHdian"
LABEL maintainer="Tautulli"
ARG VERSION
ARG BRANCH
ARG COMMIT
ENV TAUTULLI_DOCKER=True
ENV TZ=UTC
@@ -11,16 +11,8 @@ ENV TZ=UTC
WORKDIR /app
RUN \
apt-get -q -y update --no-install-recommends && \
apt-get install -q -y --no-install-recommends \
curl && \
rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir --upgrade \
pycryptodomex \
pyopenssl && \
echo ${VERSION} > /app/version.txt && \
echo ${BRANCH} > /app/branch.txt
echo ${BRANCH} > /app/branch.txt && \
echo ${COMMIT} > /app/version.txt
COPY . /app

View File

@@ -36,7 +36,7 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
| --- | --- | --- | --- |
| Release | [![Release@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![Release Date@master](https://img.shields.io/github/release-date/Tautulli/Tautulli?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/releases/latest) | [![Release@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![Commits@nightly](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/beta?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/beta) | [![Last Commits@nightly](https://img.shields.io/github/last-commit/Tautulli/Tautulli/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [![Commits@nightly](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) |
| Docker | [![Docker@master](https://img.shields.io/badge/tautulli-tautulli:latest-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker%20Branch/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Amaster) | [![Docker@beta](https://img.shields.io/badge/tautulli-tautulli:beta-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@beta](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker%20Branch/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Abeta) | [![Docker@nightly](https://img.shields.io/badge/tautulli-tautulli:nightly-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@nightly](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker%20Branch/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Anightly) |
| Docker | [![Docker@master](https://img.shields.io/badge/tautulli-tautulli:latest-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [![Docker@beta](https://img.shields.io/badge/tautulli-tautulli:beta-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [![Docker@nightly](https://img.shields.io/badge/tautulli-tautulli:nightly-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
[![Wiki](https://img.shields.io/badge/github-wiki-black?style=flat-square)](https://github.com/Tautulli/Tautulli-Wiki/wiki)
[![Discord](https://img.shields.io/discord/183396325142822912?label=discord&style=flat-square&color=7289DA)](https://tautulli.com/discord)

View File

@@ -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;
@@ -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;
@@ -3109,6 +3117,21 @@ div.dataTables_info {
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;
@@ -3798,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;
}
@@ -3910,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;
@@ -4021,7 +4043,33 @@ a:hover .overlay-refresh-image:hover {
.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;
}

View File

@@ -143,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>

View File

@@ -185,18 +185,16 @@
$('#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 },
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}

View File

@@ -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>

View File

@@ -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"
}

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -41,7 +41,7 @@ DOCUMENTATION :: END
from plexpy import notifiers
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
from plexpy.helpers import page
from plexpy.helpers import page, get_percent
# Get audio codec file
def af(codec):
@@ -269,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>
@@ -709,18 +721,16 @@ 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 },
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
@@ -848,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();
};

View File

@@ -461,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';
}

View File

@@ -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%",
@@ -317,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');
}
});
}
@@ -353,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) {
@@ -382,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);
}
@@ -549,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);
@@ -576,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);
}

View File

@@ -169,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) {

View File

@@ -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>&nbsp' +
'<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>&nbsp&nbsp&nbsp' +
'<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>&nbsp' +
'<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>&nbsp&nbsp&nbsp' +
'<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>&nbsp' +
'</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');
@@ -232,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');
}
}
}
@@ -277,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();
}
@@ -300,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) {

View File

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

View File

@@ -167,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) {

View File

@@ -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>&nbsp' +
'<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>&nbsp&nbsp&nbsp' +
'<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>&nbsp' +
'<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>&nbsp&nbsp&nbsp' +
'<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>&nbsp' +
'<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>&nbsp' +
'</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="' + page('user', 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="' + page('user', 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,7 +77,7 @@ 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'] + '">' +
$(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>');
@@ -254,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');
}
}
}
@@ -268,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);
@@ -326,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();
}
@@ -349,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) {

View File

@@ -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);
}
});
});
libraries_to_purge.forEach(function(row, idx) {
$.ajax({
url: 'delete_all_library_history',
type: 'POST',
data: { section_id: row },
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();
}
});
});
$.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();
}
});
});
}
@@ -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 {

View File

@@ -62,9 +62,21 @@ DOCUMENTATION :: END
<div class="table-card-back">
<div class="user-info-wrapper">
% 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)});"></div>
<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>
@@ -411,6 +423,8 @@ DOCUMENTATION :: END
history_table.draw();
});
$(".inactive-library-tooltip").tooltip();
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
@@ -471,18 +485,16 @@ 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 },
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}

View File

@@ -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>
* {

View File

@@ -998,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>
@@ -1239,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>
@@ -2143,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']}";
@@ -2806,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();

View File

@@ -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>

View File

@@ -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':
@@ -540,6 +546,8 @@ DOCUMENTATION :: END
login_log_table.draw();
});
$(".inactive-user-tooltip").tooltip();
% if _session['user_group'] == 'admin':
$("#edit-user-tooltip").tooltip();
@@ -566,18 +574,16 @@ 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 },
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}

View File

@@ -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);
}
});
});
users_to_purge.forEach(function(row, idx) {
$.ajax({
url: 'delete_all_user_history',
type: 'POST',
data: { user_id: row },
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();
}
});
});
$.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();
}
});
});
}
@@ -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 {

View File

@@ -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">

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -661,11 +663,11 @@ def dbcheck():
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
@@ -673,7 +675,7 @@ def dbcheck():
'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, custom_art_url TEXT, '
'count INTEGER, parent_count INTEGER, child_count INTEGER, '
'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))'
)
@@ -694,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)'
)
@@ -1731,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')
@@ -1903,6 +1917,15 @@ def dbcheck():
'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()
@@ -1989,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')

View File

@@ -120,7 +120,7 @@ class API2:
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 """

View File

@@ -341,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.'},

View File

@@ -338,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),

View File

@@ -21,6 +21,7 @@ import threading
import time
import plexpy
import helpers
import logger
FILENAME = "tautulli.db"
@@ -33,33 +34,84 @@ def integrity_check():
return result
def drop_session_db():
monitor_db = MonitorDatabase()
monitor_db.action('DROP TABLE sessions')
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')
def delete_sessions():
logger.debug(u"Tautulli Database :: Clearing temporary sessions from database.")
def clear_table(table=None):
if table:
monitor_db = MonitorDatabase()
logger.debug(u"Tautulli Database :: Clearing database table '%s'." % table)
try:
monitor_db.action('DELETE FROM sessions')
monitor_db.action('DELETE FROM %s' % table)
monitor_db.action('VACUUM')
return True
except Exception as e:
logger.warn(u"Tautulli Database :: Unable to clear temporary sessions from database: %s." % e)
logger.error(u"Tautulli Database :: Failed to clear database table '%s': %s." % (table, e))
return False
def delete_sessions():
logger.info(u"Tautulli Database :: Clearing temporary sessions from database.")
return clear_table('sessions')
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):
""" Returns the filepath to the db """
@@ -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)

View File

@@ -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',
@@ -116,7 +116,7 @@ class DataFactory(object):
columns_union = [
'NULL AS reference_id',
'NULL AS id',
'NULL AS row_id',
'started AS date',
'started',
'stopped',
@@ -228,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'],
@@ -336,7 +336,6 @@ class DataFactory(object):
'user': '',
'friendly_name': '',
'platform': '',
'platform': '',
'live': item['live'],
'guid': item['guid'],
'row_id': item['id']
@@ -1406,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()
@@ -1549,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()

View File

@@ -440,7 +440,11 @@ def create_https_certificates(ssl_cert, ssl_key):
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
"""
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())
@@ -1024,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'
@@ -1260,8 +1265,10 @@ def mask_config_passwords(config):
return config
def bool_true(value):
if value is True or value == 1:
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

View File

@@ -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()
@@ -289,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',
@@ -303,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',
@@ -322,7 +333,8 @@ class Libraries(object):
'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',
@@ -361,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'],
@@ -372,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'],
@@ -388,7 +402,8 @@ class Libraries(object):
'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)
@@ -725,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,
@@ -733,6 +750,7 @@ class Libraries(object):
'count': 0,
'parent_count': 0,
'child_count': 0,
'is_active': 1,
'do_notify': 0,
'do_notify_created': 0,
'keep_history': 1,
@@ -747,9 +765,10 @@ class Libraries(object):
try:
if str(section_id).isdigit():
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
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, ' \
'custom_art_url AS custom_art, is_active, ' \
'do_notify, do_notify_created, keep_history, deleted_section ' \
'FROM library_sections ' \
'WHERE section_id = ? '
@@ -775,7 +794,9 @@ class Libraries(object):
else:
library_art = item['library_art']
library_details = {'section_id': item['section_id'],
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,
@@ -783,6 +804,7 @@ class Libraries(object):
'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'],
@@ -812,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():
@@ -998,61 +1024,48 @@ 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.'
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()
@@ -1062,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
@@ -1075,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

View File

@@ -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:

View File

@@ -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.")

View File

@@ -554,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() -
@@ -808,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'],
@@ -1209,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': [],
@@ -1222,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)

View File

@@ -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:

View File

@@ -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'),

View File

@@ -2365,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,
}
@@ -2643,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':

View File

@@ -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',
@@ -121,9 +129,10 @@ class Users(object):
'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',
@@ -165,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'],
@@ -189,7 +199,8 @@ class Users(object):
'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)
@@ -216,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',
@@ -276,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'],
@@ -325,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,
@@ -349,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 '
@@ -394,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'],
@@ -434,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():
@@ -601,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'
@@ -613,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'],
@@ -637,54 +664,41 @@ 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)
def delete(self, user_id=None):
monitor_db = database.MonitorDatabase()
logger.info(u"Tautulli Users :: Deleting user with user_id %s from database."
% user_id)
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.'
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)
else:
return False
def undelete(self, user_id=None, username=None):
monitor_db = database.MonitorDatabase()
@@ -694,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
@@ -706,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

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.2.0"
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.2.2-beta"

View File

@@ -251,7 +251,7 @@ def update():
logger.info('Windows .exe updating not supported yet.')
elif plexpy.INSTALL_TYPE == 'git':
output, err = runGit('pull {} {} --ff-only'.format(plexpy.CONFIG.GIT_REMOTE,
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_BRANCH))
if not output:
@@ -392,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)
@@ -409,7 +412,15 @@ 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:
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 + '>'

View File

@@ -341,6 +341,20 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Flush sessions failed.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_recently_added(self, **kwargs):
""" Flush out all of the recently added items in the database."""
result = database.delete_recently_added()
if result:
return {'result': 'success', 'message': 'Recently added flushed.'}
else:
return {'result': 'error', 'message': 'Flush recently added failed.'}
##### Libraries #####
@@ -383,7 +397,8 @@ class WebInterface(object):
"do_notify_created": "Checked",
"duration": 1578037,
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1128,
"histroy_row_id": 1128,
"is_active": 1,
"keep_history": "Checked",
"labels": [],
"last_accessed": 1462693216,
@@ -399,9 +414,11 @@ class WebInterface(object):
"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
},
@@ -427,6 +444,8 @@ class WebInterface(object):
("duration", True, False)]
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "section_name")
grouping = helpers.bool_true(grouping, return_none=True)
library_data = libraries.Libraries()
library_list = library_data.get_datatables_list(kwargs=kwargs, grouping=grouping)
@@ -772,13 +791,16 @@ class WebInterface(object):
"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"
}
```
"""
@@ -796,7 +818,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_library_watch_time_stats(self, section_id=None, grouping=None, **kwargs):
def get_library_watch_time_stats(self, section_id=None, grouping=None, query_days=None, **kwargs):
""" Get a library's watch time statistics.
```
@@ -805,6 +827,7 @@ class WebInterface(object):
Optional parameters:
grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns:
json:
@@ -827,11 +850,12 @@ class WebInterface(object):
]
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
if section_id:
library_data = libraries.Libraries()
result = library_data.get_watch_time_stats(section_id=section_id, grouping=grouping)
result = library_data.get_watch_time_stats(section_id=section_id, grouping=grouping,
query_days=query_days)
if result:
return result
else:
@@ -870,7 +894,7 @@ class WebInterface(object):
]
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
if section_id:
library_data = libraries.Libraries()
@@ -886,7 +910,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_all_library_history(self, section_id, **kwargs):
def delete_all_library_history(self, server_id=None, section_id=None, row_ids=None, **kwargs):
""" Delete all Tautulli history for a specific library.
```
@@ -894,27 +918,28 @@ class WebInterface(object):
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
```
"""
if (server_id and section_id) or row_ids:
library_data = libraries.Libraries()
if section_id:
delete_row = library_data.delete_all_history(section_id=section_id)
if delete_row:
return {'message': delete_row}
success = library_data.delete(server_id=server_id, section_id=section_id, row_ids=row_ids, purge_only=True)
if success:
return {'result': 'success', 'message': 'Deleted library history.'}
else:
return {'message': 'no data received'}
return {'result': 'error', 'message': 'Failed to delete library(s) history.'}
else:
return {'result': 'error', 'message': 'No server id and section id or row ids received.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_library(self, section_id, **kwargs):
def delete_library(self, server_id=None, section_id=None, row_ids=None, **kwargs):
""" Delete a library section from Tautulli. Also erases all history for the library.
```
@@ -922,21 +947,22 @@ class WebInterface(object):
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
```
"""
if (server_id and section_id) or row_ids:
library_data = libraries.Libraries()
if section_id:
delete_row = library_data.delete(section_id=section_id)
if delete_row:
return {'message': delete_row}
success = library_data.delete(server_id=server_id, section_id=section_id, row_ids=row_ids)
if success:
return {'result': 'success', 'message': 'Deleted library.'}
else:
return {'message': 'no data received'}
return {'result': 'error', 'message': 'Failed to delete library(s).'}
else:
return {'result': 'error', 'message': 'No server id and section id or row ids received.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -1052,8 +1078,9 @@ class WebInterface(object):
"duration": 2998290,
"friendly_name": "Jon Snow",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1121,
"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,
@@ -1067,6 +1094,7 @@ class WebInterface(object):
"player": "Plex Web (Chrome)",
"plays": 487,
"rating_key": 153037,
"row_id": 1,
"thumb": "/library/metadata/153036/thumb/1462175062",
"transcode_decision": "transcode",
"user_id": 133788,
@@ -1094,6 +1122,8 @@ class WebInterface(object):
("duration", True, False)]
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "friendly_name")
grouping = helpers.bool_true(grouping, return_none=True)
user_data = users.Users()
user_list = user_data.get_datatables_list(kwargs=kwargs, grouping=grouping)
@@ -1388,10 +1418,13 @@ class WebInterface(object):
"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",
@@ -1413,7 +1446,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_user_watch_time_stats(self, user_id=None, grouping=None, **kwargs):
def get_user_watch_time_stats(self, user_id=None, grouping=None, query_days=None, **kwargs):
""" Get a user's watch time statistics.
```
@@ -1422,6 +1455,7 @@ class WebInterface(object):
Optional parameters:
grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns:
json:
@@ -1444,11 +1478,11 @@ class WebInterface(object):
]
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
if user_id:
user_data = users.Users()
result = user_data.get_watch_time_stats(user_id=user_id, grouping=grouping)
result = user_data.get_watch_time_stats(user_id=user_id, grouping=grouping, query_days=query_days)
if result:
return result
else:
@@ -1487,7 +1521,7 @@ class WebInterface(object):
]
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
if user_id:
user_data = users.Users()
@@ -1503,7 +1537,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_all_user_history(self, user_id, **kwargs):
def delete_all_user_history(self, user_id=None, row_ids=None, **kwargs):
""" Delete all Tautulli history for a specific user.
```
@@ -1511,25 +1545,27 @@ class WebInterface(object):
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
```
"""
if user_id:
if user_id or row_ids:
user_data = users.Users()
delete_row = user_data.delete_all_history(user_id=user_id)
if delete_row:
return {'message': delete_row}
success = user_data.delete(user_id=user_id, row_ids=row_ids, purge_only=True)
if success:
return {'result': 'success', 'message': 'Deleted user history.'}
else:
return {'message': 'no data received'}
return {'result': 'error', 'message': 'Failed to delete user(s) history.'}
else:
return {'result': 'error', 'message': 'No user id or row ids received.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_user(self, user_id, **kwargs):
def delete_user(self, user_id=None, row_ids=None, **kwargs):
""" Delete a user from Tautulli. Also erases all history for the user.
```
@@ -1537,19 +1573,21 @@ class WebInterface(object):
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
```
"""
if user_id:
if user_id or row_ids:
user_data = users.Users()
delete_row = user_data.delete(user_id=user_id)
if delete_row:
return {'message': delete_row}
success = user_data.delete(user_id=user_id, row_ids=row_ids)
if success:
return {'result': 'success', 'message': 'Deleted user.'}
else:
return {'message': 'no data received'}
return {'result': 'error', 'message': 'Failed to delete user(s).'}
else:
return {'result': 'error', 'message': 'No user id or row ids received.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -1637,7 +1675,6 @@ class WebInterface(object):
"group_count": 1,
"group_ids": "1124",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1124,
"ip_address": "xxx.xxx.xxx.xxx",
"live": 0,
"media_index": 17,
@@ -1653,6 +1690,7 @@ class WebInterface(object):
"player": "Castle-PC",
"rating_key": 4348,
"reference_id": 1123,
"row_id": 1124,
"session_key": null,
"started": 1462688107,
"state": null,
@@ -1689,10 +1727,7 @@ class WebInterface(object):
("watched_status", False, False)]
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "date")
if grouping and str(grouping).isdigit():
grouping = int(grouping)
elif grouping == 'false':
grouping = 0
grouping = helpers.bool_true(grouping, return_none=True)
custom_where = []
if user_id:
@@ -1830,16 +1865,32 @@ class WebInterface(object):
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def delete_history_rows(self, row_id, **kwargs):
@addtoapi("delete_history")
def delete_history_rows(self, row_ids=None, **kwargs):
""" 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
```
"""
data_factory = datafactory.DataFactory()
if row_id:
delete_row = data_factory.delete_session_history_rows(row_id=row_id)
if row_ids:
success = database.delete_session_history_rows(row_ids=row_ids)
if delete_row:
return {'message': delete_row}
if success:
return {'result': 'success', 'message': 'Deleted history.'}
else:
return {'message': 'no data received'}
return {'result': 'error', 'message': 'Failed to delete history.'}
else:
return {'result': 'error', 'message': 'No row ids received.'}
##### Graphs #####
@@ -1908,10 +1959,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_per_day(time_range=time_range, user_id=user_id, y_axis=y_axis, grouping=grouping)
result = graph.get_total_plays_per_day(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -1948,10 +2002,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_per_dayofweek(time_range=time_range, user_id=user_id, y_axis=y_axis)
result = graph.get_total_plays_per_dayofweek(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -1988,10 +2045,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_per_hourofday(time_range=time_range, user_id=user_id, y_axis=y_axis)
result = graph.get_total_plays_per_hourofday(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2028,10 +2088,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_per_month(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_total_plays_per_month(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2042,7 +2105,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by top 10 platforms.
```
@@ -2068,10 +2131,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_by_top_10_platforms(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_total_plays_by_top_10_platforms(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2082,7 +2148,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_plays_by_top_10_users(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by top 10 users.
```
@@ -2108,10 +2174,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_by_top_10_users(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_total_plays_by_top_10_users(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2122,7 +2191,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_plays_by_stream_type(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_plays_by_stream_type(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by stream type by date.
```
@@ -2147,10 +2216,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_per_stream_type(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_total_plays_per_stream_type(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2161,7 +2233,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_plays_by_source_resolution(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by source resolution.
```
@@ -2186,10 +2258,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_by_source_resolution(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_total_plays_by_source_resolution(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2200,7 +2275,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_plays_by_stream_resolution(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_plays_by_stream_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by stream resolution.
```
@@ -2225,10 +2300,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_total_plays_by_stream_resolution(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_total_plays_by_stream_resolution(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2239,7 +2317,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by stream type by top 10 users.
```
@@ -2264,10 +2342,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_stream_type_by_top_10_users(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_stream_type_by_top_10_users(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -2278,7 +2359,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by stream type by top 10 platforms.
```
@@ -2303,10 +2384,13 @@ class WebInterface(object):
}
```
"""
grouping = int(grouping) if str(grouping).isdigit() else grouping
grouping = helpers.bool_true(grouping, return_none=True)
graph = graphs.Graphs()
result = graph.get_stream_type_by_top_10_platforms(time_range=time_range, y_axis=y_axis, user_id=user_id)
result = graph.get_stream_type_by_top_10_platforms(time_range=time_range,
y_axis=y_axis,
user_id=user_id,
grouping=grouping)
if result:
return result
@@ -3503,12 +3587,12 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def verify_mobile_device(self, device_token='', cancel=False, **kwargs):
if helpers.bool_true(cancel):
mobile_app.TEMP_DEVICE_TOKEN = None
mobile_app.set_temp_device_token(None)
return {'result': 'error', 'message': 'Device registration cancelled.'}
result = mobile_app.get_mobile_device_by_token(device_token)
if result:
mobile_app.TEMP_DEVICE_TOKEN = None
mobile_app.set_temp_device_token(None)
return {'result': 'success', 'message': 'Device registered successfully.', 'data': result}
else:
return {'result': 'error', 'message': 'Device not registered.'}
@@ -3795,7 +3879,7 @@ class WebInterface(object):
logger._BLACKLIST_WORDS.add(apikey)
if helpers.bool_true(device):
mobile_app.TEMP_DEVICE_TOKEN = apikey
mobile_app.set_temp_device_token(apikey)
return apikey
@@ -3956,17 +4040,16 @@ class WebInterface(object):
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL
}
if source == 'history':
data_factory = datafactory.DataFactory()
metadata = data_factory.get_metadata_details(rating_key=rating_key, guid=guid)
if metadata:
poster_info = data_factory.get_poster_info(metadata=metadata)
metadata.update(poster_info)
lookup_info = data_factory.get_lookup_info(metadata=metadata)
metadata.update(lookup_info)
else:
# Try to get metadata from the Plex server first
if rating_key:
pms_connect = pmsconnect.PmsConnect()
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
# If the item is not found on the Plex server, get the metadata from history
if not metadata and source == 'history':
data_factory = datafactory.DataFactory()
metadata = data_factory.get_metadata_details(rating_key=rating_key, guid=guid)
if metadata:
data_factory = datafactory.DataFactory()
poster_info = data_factory.get_poster_info(metadata=metadata)
@@ -3978,7 +4061,8 @@ class WebInterface(object):
if metadata['section_id'] and not allow_session_library(metadata['section_id']):
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
return serve_template(templatename="info.html", metadata=metadata, title="Info", config=config, source=source)
return serve_template(templatename="info.html", metadata=metadata, title="Info",
config=config, source=source)
else:
if get_session_user_id():
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
@@ -4345,15 +4429,18 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_lookup_info(self, rating_key='', title='', **kwargs):
def delete_lookup_info(self, rating_key='', service='', delete_all=False, **kwargs):
""" 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:
@@ -4363,7 +4450,7 @@ class WebInterface(object):
"""
data_factory = datafactory.DataFactory()
result = data_factory.delete_lookup_info(rating_key=rating_key, title=title)
result = data_factory.delete_lookup_info(rating_key=rating_key, service=service, delete_all=delete_all)
if result:
return {'result': 'success', 'message': 'Deleted lookup info.'}
@@ -5302,6 +5389,7 @@ class WebInterface(object):
[{"art": "/:/resources/show-fanart.jpg",
"child_count": "3745",
"count": "62",
"is_active": 1,
"parent_count": "240",
"section_id": "2",
"section_name": "TV Shows",
@@ -5345,11 +5433,13 @@ class WebInterface(object):
"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",
@@ -5440,7 +5530,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_home_stats(self, grouping=0, time_range=30, stats_type='plays', stats_count=10, **kwargs):
def get_home_stats(self, time_range=30, stats_type='plays', stats_count=10, grouping=None, **kwargs):
""" Get the homepage watch statistics.
```
@@ -5522,6 +5612,8 @@ class WebInterface(object):
elif stats_type in (1, '1'):
stats_type = 'duration'
grouping = helpers.bool_true(grouping, return_none=True)
data_factory = datafactory.DataFactory()
result = data_factory.get_home_stats(grouping=grouping,
time_range=time_range,