Compare commits
393 Commits
v2.5.5
...
3fb7f4ddd1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb7f4ddd1 | |||
| 38f0a44fa0 | |||
| af3c826f7d | |||
| cbee080b54 | |||
| ecbf38c005 | |||
| 91e28df1a5 | |||
| 1df28243c3 | |||
| b867dc9be2 | |||
|
|
97f80adf0b | ||
|
|
2fc7b08909 | ||
|
|
defceed696 | ||
|
|
249533ac51 | ||
|
|
12aee8762e | ||
|
|
d9325b7adf | ||
|
|
4975cad4fa | ||
|
|
33fc079318 | ||
|
|
b3b2752554 | ||
|
|
505cf25ca3 | ||
|
|
9747e3ba98 | ||
|
|
729191722a | ||
|
|
ff2cf73f23 | ||
|
|
9c4d97c0f8 | ||
|
|
be911e7700 | ||
|
|
00629c0983 | ||
|
|
52ebc9a908 | ||
|
|
a029d6a931 | ||
|
|
7641e3b081 | ||
|
|
b54210480f | ||
|
|
0d9c1c640e | ||
|
|
7f84353c69 | ||
|
|
c319a4a5cc | ||
|
|
60f13df992 | ||
|
|
dea51e32a5 | ||
|
|
7019f5618b | ||
|
|
9106c068ac | ||
|
|
0b845294fb | ||
|
|
7e850dd88d | ||
|
|
877bf7060e | ||
|
|
9326d03a57 | ||
|
|
4787f42d2e | ||
|
|
56a9ccd818 | ||
|
|
1019fecc9e | ||
|
|
1855f93c1c | ||
|
|
52e6a44aa4 | ||
|
|
0b77808af6 | ||
|
|
9233ed5c53 | ||
|
|
ee68c0f622 | ||
|
|
366823cee9 | ||
|
|
40e1eb9a49 | ||
|
|
1af419a860 | ||
|
|
397f18c435 | ||
|
|
2e5dd05a6c | ||
|
|
a9fb8ddfb8 | ||
|
|
562c726787 | ||
|
|
5f82c1dc17 | ||
|
|
222800bdb6 | ||
|
|
5dd3636571 | ||
|
|
2296a9fbb3 | ||
|
|
63b5a7c036 | ||
|
|
b74ca2670e | ||
|
|
393f4e0e58 | ||
|
|
3a9ca29e99 | ||
|
|
32995fef24 | ||
|
|
a73c99fc64 | ||
|
|
a5834470ba | ||
|
|
da3bc127dc | ||
|
|
0dddc4d58f | ||
|
|
a4d5d9157b | ||
|
|
c70d5d4398 | ||
|
|
7c08b07ef5 | ||
|
|
e426b5dd35 | ||
|
|
2fdf619582 | ||
|
|
d9eed14b7a | ||
|
|
8230ffb8a4 | ||
|
|
7098930b19 | ||
|
|
56244245a4 | ||
|
|
dd2f12fa8e | ||
|
|
9598247a0d | ||
|
|
230ee90b1c | ||
|
|
e705bedc91 | ||
|
|
b5ebe7590c | ||
|
|
6d0831ceaa | ||
|
|
19e00ee2f2 | ||
|
|
80723d224e | ||
|
|
0c82bb023a | ||
|
|
0a86f24095 | ||
|
|
b41249cfa8 | ||
|
|
6659802689 | ||
|
|
964c503223 | ||
|
|
15568bf20a | ||
|
|
d10cd324bb | ||
|
|
2a22ab8c33 | ||
|
|
ca736cdae2 | ||
|
|
d589c57dd2 | ||
|
|
9b0caf2a47 | ||
|
|
f8b00bbd67 | ||
|
|
91a8c0e7a0 | ||
|
|
2089172384 | ||
|
|
1ab87e5334 | ||
|
|
b5e6861032 | ||
|
|
189930918a | ||
|
|
ff1bd0a4b8 | ||
|
|
e544d0dd07 | ||
|
|
3e0b240154 | ||
|
|
199119cafb | ||
|
|
89ab665923 | ||
|
|
dfb60de6d2 | ||
|
|
da8d41868d | ||
|
|
e9db43ebf6 | ||
|
|
c0453eae47 | ||
|
|
a8863a5aeb | ||
|
|
a8adad7dbb | ||
|
|
4cfa5ac10b | ||
|
|
55090ddeaa | ||
|
|
14346b0e69 | ||
|
|
ac24acf9ce | ||
|
|
4cde62fde9 | ||
|
|
7489bc8d98 | ||
|
|
cde9287d85 | ||
|
|
558023e18e | ||
|
|
8157ee7811 | ||
|
|
d746d2913f | ||
|
|
0136fc6436 | ||
|
|
7ce280cb92 | ||
|
|
0209fa87aa | ||
|
|
62cc2f769f | ||
|
|
a49d44c880 | ||
|
|
dab288380a | ||
|
|
2ac5c35065 | ||
|
|
ec9e2fe0f0 | ||
|
|
ecbe79b5b9 | ||
|
|
c4ac03738b | ||
|
|
352dbd9bc8 | ||
|
|
393b395df0 | ||
|
|
1a96da04a1 | ||
|
|
615b98955a | ||
|
|
11b2b67f9d | ||
|
|
0e44255e6a | ||
|
|
a649d2ec12 | ||
|
|
8a953e789c | ||
|
|
317d32eb0c | ||
|
|
b2ddedc0ae | ||
|
|
620d2cf730 | ||
|
|
2578592cc7 | ||
|
|
44c643d7da | ||
|
|
39d6edd581 | ||
|
|
5a14b5bc35 | ||
|
|
82d9719eee | ||
|
|
401b75a76b | ||
|
|
ab75628cf7 | ||
|
|
57d08e231c | ||
|
|
f6b800c372 | ||
|
|
26773ac67f | ||
|
|
5b63cb38ae | ||
|
|
30b655a32a | ||
|
|
f3fa9601c0 | ||
|
|
034ad05383 | ||
|
|
7b936fd664 | ||
|
|
a120f52e0d | ||
|
|
58f2d22ef4 | ||
|
|
962777284a | ||
|
|
d4c8066209 | ||
|
|
9a9db88efd | ||
|
|
f5ad9cfe14 | ||
|
|
d268a7aa23 | ||
|
|
1896239bd3 | ||
|
|
07c71750d5 | ||
|
|
d5171109f5 | ||
|
|
36aa795c52 | ||
|
|
7914f56ec3 | ||
|
|
a39c6c1047 | ||
|
|
270e07341a | ||
|
|
da7c66f414 | ||
|
|
d97b87d9cc | ||
|
|
bee3361ace | ||
|
|
276ea4dd98 | ||
|
|
dd45b47032 | ||
|
|
1fd4ec3ca3 | ||
|
|
1e807af2d4 | ||
|
|
88a5db05b7 | ||
|
|
e6c8bd0c13 | ||
|
|
3be9c84f2b | ||
|
|
881f37f731 | ||
|
|
4d37f2bab2 | ||
|
|
3217c2da0b | ||
|
|
280ae04b3d | ||
|
|
d5705a52e9 | ||
|
|
43ab2f22a8 | ||
|
|
8bb40036bc | ||
|
|
8ee934404f | ||
|
|
aa9dbafa28 | ||
|
|
2b8fea8bf8 | ||
|
|
985f4293b3 | ||
|
|
22b162b3c4 | ||
|
|
7f3d8cfb8d | ||
|
|
9a7627e35e | ||
|
|
f141c67ceb | ||
|
|
e4372644e1 | ||
|
|
8552b00be4 | ||
|
|
978fea5dde | ||
|
|
b3eeaeeda5 | ||
|
|
0f2ac5104e | ||
|
|
f7766fff14 | ||
|
|
b7c2e42190 | ||
|
|
56472f8dd5 | ||
|
|
969934b8c0 | ||
|
|
62f153acd2 | ||
|
|
b53f16645c | ||
|
|
6c2786dd78 | ||
|
|
64a9b0e622 | ||
|
|
3d05a74ef4 | ||
|
|
3a1c92944f | ||
|
|
6b34b82f52 | ||
|
|
322c090d8a | ||
|
|
4bb49f9836 | ||
|
|
20566168a1 | ||
|
|
03bf4a9ef8 | ||
|
|
842a76aae1 | ||
|
|
e3214946a3 | ||
|
|
36f877c7ff | ||
|
|
6e41b7ef3d | ||
|
|
1fc9a9bcea | ||
|
|
3a439cb81c | ||
|
|
e8b0de0320 | ||
|
|
4d033bb379 | ||
|
|
fffd1ffda3 | ||
|
|
151f23fd92 | ||
|
|
25572d6a5b | ||
|
|
e27efb3946 | ||
|
|
ca69293d8b | ||
|
|
f7fa773ec7 | ||
|
|
f84c4ca73c | ||
|
|
f9d828ea67 | ||
|
|
739c977cd7 | ||
|
|
14b98a32e0 | ||
|
|
34c9ede9c9 | ||
|
|
be9f06795d | ||
|
|
ea9904bd56 | ||
|
|
501f08dd5e | ||
|
|
a985cec9c2 | ||
|
|
c8b0ff22f6 | ||
|
|
3cc8c1f8c5 | ||
|
|
5e8b946571 | ||
|
|
dd4c0d24b7 | ||
|
|
fc39f1521d | ||
|
|
60cadb1e11 | ||
|
|
28c745c19c | ||
|
|
900b524672 | ||
|
|
627129dd95 | ||
|
|
8d18e98ca7 | ||
|
|
0ba755e463 | ||
|
|
72215a9f44 | ||
|
|
2803a6095b | ||
|
|
d1172f4975 | ||
|
|
478c4540b1 | ||
|
|
cc1076e122 | ||
|
|
15e928ecf2 | ||
|
|
2c360b6472 | ||
|
|
f5c99f712a | ||
|
|
f151bb1451 | ||
|
|
1061c334ae | ||
|
|
c5ea50d480 | ||
|
|
84207effab | ||
|
|
b568af0a90 | ||
|
|
11f2f8ff81 | ||
|
|
454235dd9a | ||
|
|
ad8dee3c47 | ||
|
|
5dc0d5536d | ||
|
|
86699ece8e | ||
|
|
e3eca5af46 | ||
|
|
e9f464e34d | ||
|
|
97775e2a3b | ||
|
|
54433c43e6 | ||
|
|
3376908710 | ||
|
|
284f77b9ae | ||
|
|
2fe49f316f | ||
|
|
1428a2485f | ||
|
|
3a1b4e34aa | ||
|
|
26fb9a6803 | ||
|
|
31e6f4282d | ||
|
|
6bc7de7a6d | ||
|
|
d9ece291b7 | ||
|
|
0203a1d4dc | ||
|
|
221d6e136a | ||
|
|
ad6e314343 | ||
|
|
9fc4dbc6d6 | ||
|
|
5dade92221 | ||
|
|
4e29960238 | ||
|
|
3973c57020 | ||
|
|
dd1dc00430 | ||
|
|
9a7d6ea7d7 | ||
|
|
02d4a3b9fe | ||
|
|
0a60d5f2b2 | ||
|
|
13ff8f3a84 | ||
|
|
3efee000ce | ||
|
|
5915937975 | ||
|
|
c7621a9e36 | ||
|
|
28e2463c4f | ||
|
|
7016d3feea | ||
|
|
44b4c10bf9 | ||
|
|
be82c8f6d9 | ||
|
|
acebf96d2f | ||
|
|
27c5061d17 | ||
|
|
fcd034da00 | ||
|
|
4ee9dbab41 | ||
|
|
112811190e | ||
|
|
746295aa16 | ||
|
|
693c0ba658 | ||
|
|
e9f37d578e | ||
|
|
8f4da14611 | ||
|
|
063b7ce7cc | ||
|
|
b7243271f3 | ||
|
|
395ab97191 | ||
|
|
76da200794 | ||
|
|
47695debdd | ||
|
|
a5a2ba9d85 | ||
|
|
3fa601db3e | ||
|
|
b60dcb2a23 | ||
|
|
1e173c6eeb | ||
|
|
adb11db317 | ||
|
|
068cb51635 | ||
|
|
b1eab8bb0d | ||
|
|
2a4b48d0fa | ||
|
|
a8e0502b41 | ||
|
|
ccf0e0dae7 | ||
|
|
bfa4d3dfec | ||
|
|
93997c11dc | ||
|
|
7ce92d5f17 | ||
|
|
9740010368 | ||
|
|
e4e0b765b6 | ||
|
|
51d1dccb42 | ||
|
|
6f362ee2ad | ||
|
|
f77bbda5ac | ||
|
|
dceeaa77c5 | ||
|
|
d7c96d46e0 | ||
|
|
b9f5251188 | ||
|
|
75cdc2c5e8 | ||
|
|
7eedb14834 | ||
|
|
ca06154805 | ||
|
|
35cdef1340 | ||
|
|
d609c0daeb | ||
|
|
db0b157d43 | ||
|
|
906aedd2f1 | ||
|
|
07a9bdbde3 | ||
|
|
588b1b1bc3 | ||
|
|
eb63f89b1f | ||
|
|
fb81d1b6f3 | ||
|
|
b897212050 | ||
|
|
3f6612fe9a | ||
|
|
bf1a59c5c0 | ||
|
|
6fb3a3a3c8 | ||
|
|
ed454b2a4a | ||
|
|
27f828e619 | ||
|
|
bde0ce20d8 | ||
|
|
28c6163a31 | ||
|
|
14bb377794 | ||
|
|
61c692ad4e | ||
|
|
42856e5ac8 | ||
|
|
e82ad09a8d | ||
|
|
40fbc55ab3 | ||
|
|
a27a5b023b | ||
|
|
a7eb563c2e | ||
|
|
43fefcf748 | ||
|
|
621fb95227 | ||
|
|
d3704fcee6 | ||
|
|
55100dfb7a | ||
|
|
010fefcbbc | ||
|
|
7627f025ed | ||
|
|
e256d2080d | ||
|
|
58292067f0 | ||
|
|
d5e91801d6 | ||
|
|
91d545f480 | ||
|
|
9c2599acbe | ||
|
|
06341ee632 | ||
|
|
bcc693e4c7 | ||
|
|
8b8afacaea | ||
|
|
deb49d7ff9 | ||
|
|
6334ffa197 | ||
|
|
b872cce2a4 | ||
|
|
de2e2ee962 | ||
|
|
5468676811 | ||
|
|
c102020698 | ||
|
|
0ff363b6ee | ||
|
|
c324cf69ed | ||
|
|
721cf5c930 | ||
|
|
f07bcca96a | ||
|
|
056d0d81ac | ||
|
|
38ccd37867 | ||
|
|
21799116c5 | ||
|
|
60bdf1d1ce | ||
|
|
02658759ea | ||
|
|
68946ceede | ||
|
|
81ee44b60f |
@@ -1,10 +0,0 @@
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
contrib
|
||||
init-scripts
|
||||
package
|
||||
pylintrc
|
||||
*.md
|
||||
!CHANGELOG*.md
|
||||
start.bat
|
||||
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
github: JonnyWong16
|
||||
patreon: Tautulli
|
||||
custom: ["https://bit.ly/2InPp15"]
|
||||
95
.github/workflows/publish-docker.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: Publish Docker
|
||||
on:
|
||||
push:
|
||||
branches: [master, beta, nightly, python3]
|
||||
tags: [v*]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
|
||||
elif [[ $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo ::set-output name=tag::latest
|
||||
else
|
||||
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-output name=branch::master
|
||||
else
|
||||
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
echo ::set-output name=commit::${GITHUB_SHA}
|
||||
echo ::set-output name=build_date::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64,linux/arm
|
||||
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v3
|
||||
with:
|
||||
buildx-version: latest
|
||||
|
||||
- name: Cache Docker Layers
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Docker Buildx (no push)
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||
--output "type=image,push=false" \
|
||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
|
||||
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
|
||||
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
|
||||
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
|
||||
--file Dockerfile .
|
||||
|
||||
- name: Docker Login
|
||||
if: success()
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Docker Buildx (push)
|
||||
if: success()
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||
--output "type=image,push=true" \
|
||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
|
||||
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
|
||||
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
|
||||
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
|
||||
--file Dockerfile .
|
||||
|
||||
- name: Clear
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ${HOME}/.docker/config.json
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
||||
204
.github/workflows/publish-release.yml
vendored
@@ -1,204 +0,0 @@
|
||||
name: Publish Release
|
||||
on:
|
||||
push:
|
||||
branches: [master, beta, nightly, python3]
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION_NSIS=${GITHUB_REF#refs/tags/v}.1
|
||||
echo ::set-output name=VERSION_NSIS::${VERSION_NSIS/%-beta.1/.0}
|
||||
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
else
|
||||
echo ::set-output name=VERSION_NSIS::0.0.0.0
|
||||
echo ::set-output name=VERSION::0.0.0
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||
fi
|
||||
echo $GITHUB_SHA > version.txt
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
|
||||
restore-keys: ${{ runner.os }}-pip-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r package/requirements-windows.txt
|
||||
|
||||
- name: Build Package
|
||||
run: |
|
||||
pyinstaller -y ./package/Tautulli-windows.spec
|
||||
|
||||
- name: Create Installer
|
||||
uses: joncloud/makensis-action@v1.2
|
||||
with:
|
||||
script-file: ./package/Tautulli.nsi
|
||||
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
include-more-plugins: true
|
||||
include-custom-plugins-path: package/nsis-plugins
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-windows-installer
|
||||
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
title: Build Windows Installer
|
||||
nofail: true
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-env name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
else
|
||||
echo ::set-env name=VERSION::0.0.0
|
||||
echo ::set-output name=VERSION::0.0.0
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||
fi
|
||||
echo $GITHUB_SHA > version.txt
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/Library/Caches/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
|
||||
restore-keys: ${{ runner.os }}-pip-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r package/requirements-macos.txt
|
||||
|
||||
- name: Build Package
|
||||
run: |
|
||||
pyinstaller -y ./package/Tautulli-macos.spec
|
||||
|
||||
- name: Create Installer
|
||||
run: |
|
||||
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-macos-installer
|
||||
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
title: Build MacOS Installer
|
||||
nofail: true
|
||||
|
||||
release:
|
||||
needs: [build-windows, build-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/') && always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
run: |
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
|
||||
- name: Download Windows Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Tautulli-windows-installer
|
||||
|
||||
- name: Download MacOS Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Tautulli-macos-installer
|
||||
|
||||
- name: Get Changelog
|
||||
id: get_changelog
|
||||
run: echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
||||
release_name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
||||
body: |
|
||||
## Changelog
|
||||
|
||||
##${{ steps.get_changelog.outputs.CHANGELOG }}
|
||||
draft: false
|
||||
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
|
||||
|
||||
- name: Upload Windows Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
|
||||
- name: Upload MacOS Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_content_type: application/vnd.apple.installer+xml
|
||||
340
.gitignore
vendored
@@ -1,82 +1,280 @@
|
||||
|
||||
# Compiled source #
|
||||
###################
|
||||
*.pyc
|
||||
*.py~
|
||||
*.pyproj
|
||||
*.sln
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python,linux,windows
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python,linux,windows
|
||||
|
||||
# PlexPy files #
|
||||
######################
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### PyCharm+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### PyCharm+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytestdebug.log
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
*.db*
|
||||
*.db-journal
|
||||
*.ini
|
||||
release.lock
|
||||
version.lock
|
||||
logs/*
|
||||
backups/*
|
||||
cache/*
|
||||
newsletters/*
|
||||
*.mmdb
|
||||
version.txt
|
||||
branch.txt
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# HTTPS Cert/Key #
|
||||
##################
|
||||
/*.crt
|
||||
/*.key
|
||||
/*.csr
|
||||
/*.pem
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Mergetool
|
||||
*.orgin
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store?
|
||||
.DS_Store
|
||||
ehthumbs.db
|
||||
Icon?
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
doc/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
pythonenv*
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# profiling data
|
||||
.prof
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
#Ignore files generated by PyCharm
|
||||
*.idea/*
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
#Ignore files generated by vi
|
||||
*.swp
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
#Ignore files build by Visual Studio
|
||||
*.obj
|
||||
*.exe
|
||||
*.pdb
|
||||
*.user
|
||||
*.aps
|
||||
*.pch
|
||||
*.vspscc
|
||||
*_i.c
|
||||
*_p.c
|
||||
*.ncb
|
||||
*.suo
|
||||
*.tlb
|
||||
*.tlh
|
||||
*.bak
|
||||
*.cache
|
||||
*.ilk
|
||||
*.log
|
||||
[Bb]in
|
||||
[Dd]ebug*/
|
||||
*.lib
|
||||
*.sbr
|
||||
obj/
|
||||
[Rr]elease*/
|
||||
_ReSharper*/
|
||||
[Tt]est[Rr]esult*
|
||||
/cache
|
||||
/logs
|
||||
.project
|
||||
.pydevproject
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
#Ignore files generated by pyinstaller
|
||||
/build
|
||||
/dist
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python,linux,windows
|
||||
/config.ini
|
||||
/release.lock
|
||||
/tautulli.db
|
||||
/version.lock
|
||||
/cache/
|
||||
|
||||
102
CHANGELOG.md
@@ -1,5 +1,107 @@
|
||||
# Changelog
|
||||
|
||||
## v2.6.5 (2021-01-09)
|
||||
|
||||
* Other:
|
||||
* Fix: Some IP addresses not being masked in the logs.
|
||||
* New: Auto-updater for Windows exe installer.
|
||||
* Change: Allow Snap package to access the user home directory.
|
||||
* Change: Migrate Snap user data to a persistent location that is retained if Tautulli is reinstalled.
|
||||
|
||||
|
||||
## v2.6.4 (2020-12-20)
|
||||
|
||||
* Other:
|
||||
* Fix: Restore Snap data folder from previous installs.
|
||||
|
||||
|
||||
## v2.6.3 (2020-12-19)
|
||||
|
||||
* Announcements:
|
||||
* This is the last Tautulli version to support Python 2. Python 3 will be required to continue receiving updates. You can check your Python version on the settings page.
|
||||
* Exporter:
|
||||
* Fix: Accessible and exists attributes were blank for media info export level 9.
|
||||
* UI:
|
||||
* Fix: Guest usernames were not masked on mouse hover.
|
||||
* Other:
|
||||
* Fix: macOS menu bar icon for light and dark mode.
|
||||
* New: Tautulli can officially be installed on Linux using a Snap package. See the installation wiki for details.
|
||||
|
||||
|
||||
## v2.6.2 (2020-12-05)
|
||||
|
||||
* Notifications:
|
||||
* Change: Send a notification of a user new device for the first time only. This can be toggled off in the settings.
|
||||
* Exporter:
|
||||
* Fix: Allow exporting child fields only without requiring the parent fields as well.
|
||||
* Fix: Exporting individual collection would fail.
|
||||
* Change: Remove accessible and exists fields from the default media info export levels. This prevents the Plex server from reading the media files unnecessarily.
|
||||
* Other:
|
||||
* Fix: Enable high resolution for the macOS system tray icon and menu.
|
||||
* New: Added rate limiting for failed login attempts.
|
||||
* Change: Use a white logo for the macOS system tray icon.
|
||||
* API:
|
||||
* New: Added machine_id to the get_history API response.
|
||||
|
||||
|
||||
## v2.6.1 (2020-11-03)
|
||||
|
||||
* Other:
|
||||
* Fix: High CPU/memory usage in some instances.
|
||||
* Fix: Logger error preventing Tautulli from starting.
|
||||
* Fix: Database issue with non-unique image hashes.
|
||||
|
||||
|
||||
## v2.6.0 (2020-10-31)
|
||||
|
||||
* Exporter:
|
||||
* New: New exporter feature that allows you to export the metadata and images for any library, collection, playlist, or media item to csv, json, xml, or m3u8. Refer to the Exporter Guide in the wiki for more details.
|
||||
* UI:
|
||||
* Fix: Margin on the homepage activity and statistic/library cards. (Thanks @dotsam)
|
||||
* Fix: Movie ratings not showing on the info page for the new Plex Movie agent.
|
||||
* New: Added ability to browse collections and playlists from the library and user pages.
|
||||
* Change: Updated platform brand logos and colours.
|
||||
* API:
|
||||
* New: Added export_metadata, download_export, and delete_export API commands.
|
||||
* New: Added get_collections_table, and get_playlists_table API commands.
|
||||
* New: Added min_version parameter to the register_device API command.
|
||||
* New: Added include_activity parameter to the get_history API command.
|
||||
* New: Added sync_id parameter to the get_metadata API command.
|
||||
* New: Added delete_synced_item API command.
|
||||
* New: Added a stat_id and stats_start parameters to the get_home_stats API command.
|
||||
* New: Allow deleting a mobile device using the registration device_id for the delete_mobile_device API command.
|
||||
* Change: Return Plex server info and Tautulli info from the register_device command.
|
||||
* Other:
|
||||
* New: The Docker container is now also built for the arm32v6 architecture.
|
||||
* New: The Docker container is also published to the GitHub Container Registry at ghcr.io/tautulli/tautulli.
|
||||
* Change: Tautulli is now using a forked version of plexapi 3.6.0. This is to support the exporter feature while still maintaining Python 2 compatibility.
|
||||
* Change: Updated systemd script to remove process forking. (Thanks @MichaIng)
|
||||
* Change: Cache GitHub update check on startup.
|
||||
|
||||
|
||||
## v2.5.6 (2020-10-02)
|
||||
|
||||
* Activity:
|
||||
* Change: Renamed container "Transcode" to "Converting" on activity cards.
|
||||
* Notifications:
|
||||
* New: Added a silent notification option for Telegram. (Thanks @JohnnyKing94)
|
||||
* New: Added container_decision notification parameter.
|
||||
* New: Added notification trigger for Playback Error.
|
||||
* New: Added remote access down notification threshold setting.
|
||||
* Newsletter:
|
||||
* Change: Stop flooring newsletter start date.
|
||||
* UI:
|
||||
* Fix: Unable to purge history from the library edit modal.
|
||||
* Fix: QR code not showing up for localhost address when trying to register a device.
|
||||
* New: Added library name to the fix metadata modal.
|
||||
* API:
|
||||
* New: Added default thumb and art to the Live TV library.
|
||||
* Other:
|
||||
* Fix: Synced items not loading for guest access.
|
||||
* New: Schedule some more automatic database optimizations.
|
||||
* Change: Added automatic uninstall before installing to the Windows installer.
|
||||
|
||||
|
||||
## v2.5.5 (2020-09-06)
|
||||
|
||||
* Activity:
|
||||
|
||||
23
Dockerfile
@@ -1,23 +0,0 @@
|
||||
FROM tautulli/tautulli-baseimage:python3
|
||||
|
||||
LABEL maintainer="Tautulli"
|
||||
|
||||
ARG BRANCH
|
||||
ARG COMMIT
|
||||
|
||||
ENV TAUTULLI_DOCKER=True
|
||||
ENV TZ=UTC
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN \
|
||||
echo ${BRANCH} > /app/branch.txt && \
|
||||
echo ${COMMIT} > /app/version.txt
|
||||
|
||||
COPY . /app
|
||||
|
||||
ENTRYPOINT [ "./start.sh" ]
|
||||
|
||||
VOLUME /config
|
||||
EXPOSE 8181
|
||||
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1
|
||||
@@ -20,31 +20,27 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure lib added to path, before any other imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
|
||||
|
||||
from future.builtins import str
|
||||
|
||||
import appdirs
|
||||
import argparse
|
||||
import datetime
|
||||
import locale
|
||||
import pytz
|
||||
import signal
|
||||
import shutil
|
||||
import time
|
||||
import threading
|
||||
import tzlocal
|
||||
|
||||
import plexpy
|
||||
from plexpy import common, config, database, helpers, logger, webstart
|
||||
import jellypy
|
||||
from jellypy import common, config, database, helpers, logger, webstart
|
||||
if common.PLATFORM == 'Windows':
|
||||
from plexpy import windows
|
||||
from jellypy import windows
|
||||
elif common.PLATFORM == 'Darwin':
|
||||
from plexpy import macos
|
||||
from jellypy import macos
|
||||
|
||||
# Register signals, such as CTRL + C
|
||||
signal.signal(signal.SIGINT, plexpy.sig_handler)
|
||||
signal.signal(signal.SIGTERM, plexpy.sig_handler)
|
||||
signal.signal(signal.SIGINT, jellypy.sig_handler)
|
||||
signal.signal(signal.SIGTERM, jellypy.sig_handler)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -55,28 +51,28 @@ def main():
|
||||
|
||||
# Fixed paths to Tautulli
|
||||
if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
|
||||
plexpy.FROZEN = True
|
||||
plexpy.FULL_PATH = os.path.abspath(sys.executable)
|
||||
plexpy.PROG_DIR = sys._MEIPASS
|
||||
jellypy.FROZEN = True
|
||||
jellypy.FULL_PATH = os.path.abspath(sys.executable)
|
||||
jellypy.PROG_DIR = sys._MEIPASS
|
||||
else:
|
||||
plexpy.FULL_PATH = os.path.abspath(__file__)
|
||||
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
|
||||
jellypy.FULL_PATH = os.path.abspath(__file__)
|
||||
jellypy.PROG_DIR = os.path.dirname(jellypy.FULL_PATH)
|
||||
|
||||
plexpy.ARGS = sys.argv[1:]
|
||||
jellypy.ARGS = sys.argv[1:]
|
||||
|
||||
# From sickbeard
|
||||
plexpy.SYS_PLATFORM = sys.platform
|
||||
plexpy.SYS_ENCODING = None
|
||||
jellypy.SYS_PLATFORM = sys.platform
|
||||
jellypy.SYS_ENCODING = None
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
|
||||
jellypy.SYS_LANGUAGE, jellypy.SYS_ENCODING = locale.getdefaultlocale()
|
||||
except (locale.Error, IOError):
|
||||
pass
|
||||
|
||||
# for OSes that are poorly configured I'll just force UTF-8
|
||||
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||
plexpy.SYS_ENCODING = 'UTF-8'
|
||||
if not jellypy.SYS_ENCODING or jellypy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||
jellypy.SYS_ENCODING = 'UTF-8'
|
||||
|
||||
# Set up and gather command line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -106,48 +102,50 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
plexpy.VERBOSE = True
|
||||
jellypy.VERBOSE = True
|
||||
if args.quiet:
|
||||
plexpy.QUIET = True
|
||||
jellypy.QUIET = True
|
||||
|
||||
# Do an intial setup of the logger.
|
||||
# Require verbose for pre-initilization to see critical errors
|
||||
logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)
|
||||
logger.initLogger(console=not jellypy.QUIET, log_dir=False, verbose=True)
|
||||
|
||||
try:
|
||||
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
|
||||
jellypy.SYS_TIMEZONE = tzlocal.get_localzone()
|
||||
except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e:
|
||||
logger.error("Could not determine system timezone: %s" % e)
|
||||
plexpy.SYS_TIMEZONE = pytz.UTC
|
||||
jellypy.SYS_TIMEZONE = pytz.UTC
|
||||
|
||||
plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z')
|
||||
jellypy.SYS_UTC_OFFSET = datetime.datetime.now(jellypy.SYS_TIMEZONE).strftime('%z')
|
||||
|
||||
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
||||
plexpy.DOCKER = True
|
||||
jellypy.DOCKER = True
|
||||
if helpers.bool_true(os.getenv('TAUTULLI_SNAP', False)):
|
||||
jellypy.SNAP = True
|
||||
|
||||
if args.dev:
|
||||
plexpy.DEV = True
|
||||
jellypy.DEV = True
|
||||
logger.debug("Tautulli is running in the dev environment.")
|
||||
|
||||
if args.daemon:
|
||||
if sys.platform == 'win32':
|
||||
logger.warn("Daemonizing not supported under Windows, starting normally")
|
||||
else:
|
||||
plexpy.DAEMON = True
|
||||
plexpy.QUIET = True
|
||||
jellypy.DAEMON = True
|
||||
jellypy.QUIET = True
|
||||
|
||||
if args.nofork:
|
||||
plexpy.NOFORK = True
|
||||
jellypy.NOFORK = True
|
||||
logger.info("Tautulli is running as a service, it will not fork when restarted.")
|
||||
|
||||
if args.pidfile:
|
||||
plexpy.PIDFILE = str(args.pidfile)
|
||||
jellypy.PIDFILE = str(args.pidfile)
|
||||
|
||||
# If the pidfile already exists, plexpy may still be running, so
|
||||
# If the pidfile already exists, jellypy may still be running, so
|
||||
# exit
|
||||
if os.path.exists(plexpy.PIDFILE):
|
||||
if os.path.exists(jellypy.PIDFILE):
|
||||
try:
|
||||
with open(plexpy.PIDFILE, 'r') as fp:
|
||||
with open(jellypy.PIDFILE, 'r') as fp:
|
||||
pid = int(fp.read())
|
||||
except IOError as e:
|
||||
raise SystemExit("Unable to read PID file: %s", e)
|
||||
@@ -157,20 +155,20 @@ def main():
|
||||
except OSError:
|
||||
logger.warn("PID file '%s' already exists, but PID %d is "
|
||||
"not running. Ignoring PID file." %
|
||||
(plexpy.PIDFILE, pid))
|
||||
(jellypy.PIDFILE, pid))
|
||||
else:
|
||||
# The pidfile exists and points to a live PID. plexpy may
|
||||
# The pidfile exists and points to a live PID. jellypy may
|
||||
# still be running, so exit.
|
||||
raise SystemExit("PID file '%s' already exists. Exiting." %
|
||||
plexpy.PIDFILE)
|
||||
jellypy.PIDFILE)
|
||||
|
||||
# The pidfile is only useful in daemon mode, make sure we can write the
|
||||
# file properly
|
||||
if plexpy.DAEMON:
|
||||
plexpy.CREATEPID = True
|
||||
if jellypy.DAEMON:
|
||||
jellypy.CREATEPID = True
|
||||
|
||||
try:
|
||||
with open(plexpy.PIDFILE, 'w') as fp:
|
||||
with open(jellypy.PIDFILE, 'w') as fp:
|
||||
fp.write("pid\n")
|
||||
except IOError as e:
|
||||
raise SystemExit("Unable to write PID file: %s", e)
|
||||
@@ -180,98 +178,107 @@ def main():
|
||||
|
||||
# Determine which data directory and config file to use
|
||||
if args.datadir:
|
||||
plexpy.DATA_DIR = args.datadir
|
||||
elif plexpy.FROZEN:
|
||||
plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
|
||||
jellypy.DATA_DIR = args.datadir
|
||||
elif jellypy.FROZEN:
|
||||
jellypy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
|
||||
else:
|
||||
plexpy.DATA_DIR = plexpy.PROG_DIR
|
||||
jellypy.DATA_DIR = jellypy.PROG_DIR
|
||||
|
||||
# Migrate Snap data dir
|
||||
if jellypy.SNAP:
|
||||
snap_common = os.environ['SNAP_COMMON']
|
||||
old_data_dir = os.path.join(snap_common, 'Tautulli')
|
||||
if os.path.exists(old_data_dir) and os.listdir(old_data_dir):
|
||||
jellypy.SNAP_MIGRATE = True
|
||||
logger.info("Migrating Snap user data.")
|
||||
shutil.move(old_data_dir, jellypy.DATA_DIR)
|
||||
|
||||
if args.config:
|
||||
config_file = args.config
|
||||
else:
|
||||
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
|
||||
config_file = os.path.join(jellypy.DATA_DIR, config.FILENAME)
|
||||
|
||||
# Try to create the DATA_DIR if it doesn't exist
|
||||
if not os.path.exists(plexpy.DATA_DIR):
|
||||
if not os.path.exists(jellypy.DATA_DIR):
|
||||
try:
|
||||
os.makedirs(plexpy.DATA_DIR)
|
||||
os.makedirs(jellypy.DATA_DIR)
|
||||
except OSError:
|
||||
raise SystemExit(
|
||||
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
|
||||
'Could not create data directory: ' + jellypy.DATA_DIR + '. Exiting....')
|
||||
|
||||
# Make sure the DATA_DIR is writeable
|
||||
if not os.access(plexpy.DATA_DIR, os.W_OK):
|
||||
if not os.access(jellypy.DATA_DIR, os.W_OK):
|
||||
raise SystemExit(
|
||||
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
|
||||
'Cannot write to the data directory: ' + jellypy.DATA_DIR + '. Exiting...')
|
||||
|
||||
# Put the database in the DATA_DIR
|
||||
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
|
||||
jellypy.DB_FILE = os.path.join(jellypy.DATA_DIR, database.FILENAME)
|
||||
|
||||
# Move 'plexpy.db' to 'tautulli.db'
|
||||
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
|
||||
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
|
||||
# Move 'jellypy.db' to 'tautulli.db'
|
||||
if os.path.isfile(os.path.join(jellypy.DATA_DIR, 'jellypy.db')) and \
|
||||
not os.path.isfile(os.path.join(jellypy.DATA_DIR, jellypy.DB_FILE)):
|
||||
try:
|
||||
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
|
||||
os.rename(os.path.join(jellypy.DATA_DIR, 'jellypy.db'), jellypy.DB_FILE)
|
||||
except OSError as e:
|
||||
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
|
||||
raise SystemExit("Unable to rename jellypy.db to tautulli.db: %s", e)
|
||||
|
||||
if plexpy.DAEMON:
|
||||
plexpy.daemonize()
|
||||
if jellypy.DAEMON:
|
||||
jellypy.daemonize()
|
||||
|
||||
# Read config and start logging
|
||||
plexpy.initialize(config_file)
|
||||
jellypy.initialize(config_file)
|
||||
|
||||
# Start the background threads
|
||||
plexpy.start()
|
||||
jellypy.start()
|
||||
|
||||
# Force the http port if neccessary
|
||||
if args.port:
|
||||
plexpy.HTTP_PORT = args.port
|
||||
logger.info('Using forced web server port: %i', plexpy.HTTP_PORT)
|
||||
jellypy.HTTP_PORT = args.port
|
||||
logger.info('Using forced web server port: %i', jellypy.HTTP_PORT)
|
||||
else:
|
||||
plexpy.HTTP_PORT = int(plexpy.CONFIG.HTTP_PORT)
|
||||
jellypy.HTTP_PORT = int(jellypy.CONFIG.HTTP_PORT)
|
||||
|
||||
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||
# and for CherryPy.
|
||||
if plexpy.CONFIG.ENABLE_HTTPS:
|
||||
if jellypy.CONFIG.ENABLE_HTTPS:
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
logger.warn("The pyOpenSSL module is missing. Install this "
|
||||
"module to enable HTTPS. HTTPS will be disabled.")
|
||||
plexpy.CONFIG.ENABLE_HTTPS = False
|
||||
jellypy.CONFIG.ENABLE_HTTPS = False
|
||||
|
||||
# Try to start the server. Will exit here is address is already in use.
|
||||
webstart.start()
|
||||
|
||||
if common.PLATFORM == 'Windows':
|
||||
if plexpy.CONFIG.SYS_TRAY_ICON:
|
||||
plexpy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
|
||||
plexpy.WIN_SYS_TRAY_ICON.start()
|
||||
if jellypy.CONFIG.SYS_TRAY_ICON:
|
||||
jellypy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
|
||||
jellypy.WIN_SYS_TRAY_ICON.start()
|
||||
windows.set_startup()
|
||||
elif common.PLATFORM == 'Darwin':
|
||||
macos.set_startup()
|
||||
|
||||
# Open webbrowser
|
||||
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
|
||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT,
|
||||
plexpy.HTTP_ROOT)
|
||||
if jellypy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not jellypy.DEV:
|
||||
jellypy.launch_browser(jellypy.CONFIG.HTTP_HOST, jellypy.HTTP_PORT,
|
||||
jellypy.HTTP_ROOT)
|
||||
|
||||
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
|
||||
if common.PLATFORM == 'Darwin' and jellypy.CONFIG.SYS_TRAY_ICON:
|
||||
if not macos.HAS_PYOBJC:
|
||||
logger.warn("The pyobjc module is missing. Install this "
|
||||
"module to enable the MacOS menu bar icon.")
|
||||
plexpy.CONFIG.SYS_TRAY_ICON = False
|
||||
jellypy.CONFIG.SYS_TRAY_ICON = False
|
||||
|
||||
if plexpy.CONFIG.SYS_TRAY_ICON:
|
||||
if jellypy.CONFIG.SYS_TRAY_ICON:
|
||||
# MacOS menu bar icon must be run on the main thread and is blocking
|
||||
# Start the rest of Tautulli on a new thread
|
||||
thread = threading.Thread(target=wait)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
||||
plexpy.MAC_SYS_TRAY_ICON.start()
|
||||
jellypy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
||||
jellypy.MAC_SYS_TRAY_ICON.start()
|
||||
else:
|
||||
wait()
|
||||
else:
|
||||
@@ -283,29 +290,29 @@ def wait():
|
||||
|
||||
# Wait endlessly for a signal to happen
|
||||
while True:
|
||||
if not plexpy.SIGNAL:
|
||||
if not jellypy.SIGNAL:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
plexpy.SIGNAL = 'shutdown'
|
||||
jellypy.SIGNAL = 'shutdown'
|
||||
else:
|
||||
logger.info('Received signal: %s', plexpy.SIGNAL)
|
||||
logger.info('Received signal: %s', jellypy.SIGNAL)
|
||||
|
||||
if plexpy.SIGNAL == 'shutdown':
|
||||
plexpy.shutdown()
|
||||
elif plexpy.SIGNAL == 'restart':
|
||||
plexpy.shutdown(restart=True)
|
||||
elif plexpy.SIGNAL == 'checkout':
|
||||
plexpy.shutdown(restart=True, checkout=True)
|
||||
elif plexpy.SIGNAL == 'reset':
|
||||
plexpy.shutdown(restart=True, reset=True)
|
||||
elif plexpy.SIGNAL == 'update':
|
||||
plexpy.shutdown(restart=True, update=True)
|
||||
if jellypy.SIGNAL == 'shutdown':
|
||||
jellypy.shutdown()
|
||||
elif jellypy.SIGNAL == 'restart':
|
||||
jellypy.shutdown(restart=True)
|
||||
elif jellypy.SIGNAL == 'checkout':
|
||||
jellypy.shutdown(restart=True, checkout=True)
|
||||
elif jellypy.SIGNAL == 'reset':
|
||||
jellypy.shutdown(restart=True, reset=True)
|
||||
elif jellypy.SIGNAL == 'update':
|
||||
jellypy.shutdown(restart=True, update=True)
|
||||
else:
|
||||
logger.error('Unknown signal. Shutting down...')
|
||||
plexpy.shutdown()
|
||||
jellypy.shutdown()
|
||||
|
||||
plexpy.SIGNAL = None
|
||||
jellypy.SIGNAL = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
63
README.md
@@ -1,64 +1,21 @@
|
||||
# Tautulli
|
||||
# JellyPy 
|
||||
|
||||
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
|
||||
A python based web application for monitoring, analytics and notifications for [Jellyfin](https://jellyfin.org/).
|
||||
|
||||
This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).
|
||||
This project is based on [Tautulli](https://github.com/Tautulli/Tautulli).
|
||||
|
||||
## Features
|
||||
JellyPy only supports Jellyfin. If you want to run Plex, head over to Tautulli.
|
||||
|
||||
* Responsive web design viewable on desktop, tablet and mobile web browsers.
|
||||
* Themed to complement Plex/Web.
|
||||
* Easy configuration setup (no separate web server required).
|
||||
* Monitor current Plex Media Server activity.
|
||||
* Fully customizable notifications for stream activity and recently added media.
|
||||
* Top statistics on home page with configurable duration and measurement metric.
|
||||
* Global watching history with search/filtering & dynamic column sorting.
|
||||
* Full user list with general information and comparison stats.
|
||||
* Individual user information including devices IP addresses.
|
||||
* Complete library statistics and media file information.
|
||||
* Rich analytics presented using Highcharts graphing.
|
||||
* Beautiful content information pages.
|
||||
* Full sync list data on all users syncing items from your library.
|
||||
* And many more!!
|
||||
## Differences to Tautulli
|
||||
|
||||
## Preview
|
||||
|
||||
* [Full preview gallery available on our website](https://tautulli.com)
|
||||
|
||||

|
||||
* Removed Google Analytics
|
||||
* Removed Python2 support
|
||||
* Removed import from varius abondonded Projects
|
||||
|
||||
## Installation & Support
|
||||
|
||||
[](https://python.org/downloads)
|
||||
[](https://hub.docker.com/r/tautulli/tautulli)
|
||||
[](https://hub.docker.com/r/tautulli/tautulli)
|
||||
|
||||
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
|
||||
| --- | --- | --- | --- |
|
||||
| Release | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/commits/beta) | [](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [](https://github.com/Tautulli/Tautulli/commits/nightly) |
|
||||
| Docker | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
|
||||
| Installer | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Amaster) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Abeta) | [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Anightly) |
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli-Wiki/wiki)
|
||||
[](https://tautulli.com/discord)
|
||||
[](https://www.reddit.com/r/Tautulli/)
|
||||
[](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242)
|
||||
|
||||
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
|
||||
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
|
||||
* Support is available on [Discord](https://tautulli.com/discord), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242).
|
||||
|
||||
## Issues & Feature Requests
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli-Issues)
|
||||
[](https://feathub.com/Tautulli/Tautulli)
|
||||
|
||||
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
|
||||
TODO
|
||||
|
||||
## License
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli/blob/master/LICENSE)
|
||||
|
||||
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
|
||||
|
||||
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
|
||||
[](https://git.harting.dev/anonfunc/JellyPy/src/branch/master/LICENSE)
|
||||
@@ -1,8 +1,8 @@
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy import version
|
||||
from plexpy.helpers import anon_url
|
||||
from plexpy.notifiers import BROWSER_NOTIFIERS
|
||||
import jellypy
|
||||
from jellypy import version
|
||||
from jellypy.helpers import anon_url
|
||||
from jellypy.notifiers import BROWSER_NOTIFIERS
|
||||
%>
|
||||
<!doctype html>
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
||||
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
|
||||
<link href="${http_root}css/selectize.min.css" rel="stylesheet" />
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">
|
||||
@@ -22,21 +24,21 @@
|
||||
${next.headIncludes()}
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
|
||||
</head>
|
||||
|
||||
<body class="content">
|
||||
@@ -48,17 +50,19 @@
|
||||
% if plexpy.UPDATE_AVAILABLE is None:
|
||||
You are running an unknown version of Tautulli.<br />
|
||||
% elif plexpy.UPDATE_AVAILABLE == 'release':
|
||||
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
|
||||
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">
|
||||
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
|
||||
% elif plexpy.UPDATE_AVAILABLE == 'commit':
|
||||
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
||||
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank" rel="noreferrer">
|
||||
newer version</a> of Tautulli is available!<br />
|
||||
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
|
||||
% endif
|
||||
% if plexpy.INSTALL_TYPE == 'docker':
|
||||
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE in ('windows', 'macos'):
|
||||
<a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE == 'snap':
|
||||
Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE == 'macos':
|
||||
<a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% else:
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% endif
|
||||
@@ -136,7 +140,7 @@
|
||||
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
|
||||
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
|
||||
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
|
||||
<li><a href="support"><i class="fa fa-fw fa-comment"></i> Support</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
|
||||
@@ -202,7 +206,7 @@ ${next.modalIncludes()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span id="incorrect-login" style="padding-right: 25px; display: none;">Incorrect username or password.</span>
|
||||
<span id="sign-in-alert" style="padding-right: 25px; display: none;"></span>
|
||||
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
||||
</div>
|
||||
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
||||
@@ -233,13 +237,14 @@ ${next.modalIncludes()}
|
||||
<li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
|
||||
<li><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab">Crypto</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to GitHub.
|
||||
</p>
|
||||
<a href="${anon_url('https://github.com/sponsors/JonnyWong16')}" target="_blank" class="btn btn-sm btn-default" style="font-weight: 600;">
|
||||
<a href="${anon_url('https://github.com/sponsors/JonnyWong16')}" target="_blank" rel="noreferrer" class="btn btn-sm btn-default" style="font-weight: 600;">
|
||||
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i> Sponsor
|
||||
</a>
|
||||
</div>
|
||||
@@ -247,18 +252,26 @@ ${next.modalIncludes()}
|
||||
<p>
|
||||
Click the button below to continue to Patreon.
|
||||
</p>
|
||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
|
||||
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
|
||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank" rel="noreferrer">
|
||||
<img src="images/become_a_patron_button.png" alt="Become a Patron" width="170" height="40">
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to PayPal.
|
||||
</p>
|
||||
<a href="${anon_url('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=6XPPKTDSX9QFL&lc=US&item_name=Tautulli¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted')}" target="_blank">
|
||||
<a href="${anon_url('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=6XPPKTDSX9QFL&lc=US&item_name=Tautulli¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted')}" target="_blank" rel="noreferrer">
|
||||
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="crypto-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to Coinbase.
|
||||
</p>
|
||||
<a href="https://blankrefer.com/?https://commerce.coinbase.com/checkout/8a9fa08c-8a38-409e-9220-868124c4ba0c" target="_blank" rel="noreferrer" class="donate-with-crypto">
|
||||
<span>Donate with Crypto</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -288,14 +301,17 @@ ${next.modalIncludes()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
|
||||
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
|
||||
<script src="${http_root}js/bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
|
||||
<script src="${http_root}js/moment-with-locales.min.js"></script>
|
||||
<script src="${http_root}js/moment-duration-format.min.js"></script>
|
||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||
<script src="${http_root}js/platform.min.js"></script>
|
||||
<script src="${http_root}js/ipaddr.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/selectize.min.js"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||
<script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -325,15 +341,17 @@ ${next.modalIncludes()}
|
||||
if (result.update === null) {
|
||||
msg = 'You are running an unknown version of Tautulli.<br />';
|
||||
} else if (result.update === true && result.release === true) {
|
||||
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />';
|
||||
msg = 'A <a href="' + result.release_url + '" target="_blank" rel="noreferrer">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />';
|
||||
} else if (result.update === true && result.release === false) {
|
||||
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
|
||||
msg = 'A <a href="' + result.compare_url + '" target="_blank" rel="noreferrer">newer version</a> of Tautulli is available!<br />' +
|
||||
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />';
|
||||
}
|
||||
if (result.install_type === 'docker') {
|
||||
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
|
||||
msg += '<a href="' + result.release_url + '" target="_blank">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
|
||||
} else if (result.install_type === 'snap') {
|
||||
msg += 'Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else if (result.install_type === 'macos') {
|
||||
msg += '<a href="' + result.release_url + '" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
|
||||
} else {
|
||||
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
}
|
||||
@@ -441,12 +459,16 @@ ${next.modalIncludes()}
|
||||
data: $(this).serialize(),
|
||||
dataType: 'json',
|
||||
statusCode: {
|
||||
200: function() {
|
||||
200: function(xhr, status) {
|
||||
window.location = "${http_root}";
|
||||
},
|
||||
401: function() {
|
||||
$('#incorrect-login').show();
|
||||
$('#username').focus();
|
||||
401: function(xhr, status) {
|
||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||
$('#username').focus();
|
||||
},
|
||||
429: function(xhr, status) {
|
||||
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
|
||||
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
|
||||
@@ -13,8 +13,8 @@ DOCUMENTATION :: END
|
||||
import os
|
||||
import sys
|
||||
import plexpy
|
||||
from plexpy import common, logger
|
||||
from plexpy.helpers import anon_url
|
||||
from jellypy import common, logger
|
||||
from jellypy.helpers import anon_url
|
||||
%>
|
||||
|
||||
<table class="config-info-table small-muted">
|
||||
@@ -22,11 +22,11 @@ DOCUMENTATION :: END
|
||||
% if plexpy.CURRENT_VERSION:
|
||||
<tr>
|
||||
<td>Git Branch:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}" target="_blank">${plexpy.CONFIG.GIT_BRANCH}</a></td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}" target="_blank" rel="noreferrer">${plexpy.CONFIG.GIT_BRANCH}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Git Commit Hash:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}" target="_blank">${plexpy.CURRENT_VERSION}</a></td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}" target="_blank" rel="noreferrer">${plexpy.CURRENT_VERSION}</a></td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
@@ -49,6 +49,10 @@ DOCUMENTATION :: END
|
||||
<td>Cache Directory:</td>
|
||||
<td>${plexpy.CONFIG.CACHE_DIR}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Export Directory:</td>
|
||||
<td>${plexpy.CONFIG.EXPORT_DIR}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Newsletter Directory:</td>
|
||||
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
|
||||
@@ -74,19 +78,19 @@ DOCUMENTATION :: END
|
||||
<tr>
|
||||
<td class="top-line">Resources:</td>
|
||||
<td class="top-line">
|
||||
<a class="no-highlight" href="${anon_url('https://tautulli.com')}" target="_blank">Tautulli Website</a> |
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Source</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="issue">GitHub Issues</a> |
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Wiki</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="feature request">FeatHub Feature Requests</a>
|
||||
<a class="no-highlight" href="${anon_url('https://tautulli.com')}" target="_blank" rel="noreferrer">Tautulli Website</a> |
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">GitHub Source</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" rel="noreferrer" data-id="issue">GitHub Issues</a> |
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">GitHub Wiki</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" rel="noreferrer" data-id="feature request">FeatHub Feature Requests</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Support:</td>
|
||||
<td>
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://tautulli.com/discord')}" target="_blank">Tautulli Discord Server</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242')}" target="_blank">Plex Forums</a>
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://tautulli.com/discord')}" target="_blank" rel="noreferrer">Tautulli Discord Server</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank" rel="noreferrer">Tautulli Subreddit</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242')}" target="_blank" rel="noreferrer">Plex Forums</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -71,7 +71,7 @@ ul.ColVis_collection {
|
||||
list-style: none;
|
||||
width: 150px;
|
||||
padding: 8px 8px 4px 8px;
|
||||
margin: 10px 0px 0px 0px;
|
||||
margin: 10px 0px 10px 0px;
|
||||
background-color: #444;
|
||||
overflow: hidden;
|
||||
z-index: 2002;
|
||||
|
||||
@@ -217,6 +217,10 @@ select.form-control:focus,
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.selectize-dropdown [data-selectable].option-disabled {
|
||||
color: #aaa;
|
||||
cursor: default;
|
||||
}
|
||||
select.form-control option {
|
||||
color: #555;
|
||||
background-color: #eee;
|
||||
@@ -475,6 +479,10 @@ fieldset[disabled] .btn-bright.active {
|
||||
}
|
||||
.btn-group select {
|
||||
margin-top: 0;
|
||||
height: 34px;
|
||||
}
|
||||
.btn-group label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.input-group-addon-form {
|
||||
display: inline-block;
|
||||
@@ -488,9 +496,6 @@ fieldset[disabled] .btn-bright.active {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
#user-selection label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.alert-edit {
|
||||
display: none;
|
||||
float: left;
|
||||
@@ -745,16 +750,17 @@ a .users-poster-face:hover {
|
||||
position: relative;
|
||||
}
|
||||
#dashboard-checking-activity,
|
||||
#dashboard-no-activity {
|
||||
#dashboard-no-activity,
|
||||
#dashboard-checking-recently-added,
|
||||
#dashboard-no-recently-added {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.dashboard-activity-instance {
|
||||
float: left;
|
||||
position: relative;
|
||||
height: 290px;
|
||||
min-width: 350px;
|
||||
max-width: 500px;
|
||||
margin-right: 25px;
|
||||
margin-left: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.dashboard-activity-container {
|
||||
@@ -1030,7 +1036,7 @@ a .users-poster-face:hover {
|
||||
height: 249px;
|
||||
}
|
||||
.dashboard-activity-container:hover .dashboard-activity-progress {
|
||||
height: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.dashboard-activity-container:hover .progress-bar {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
@@ -1155,9 +1161,8 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
||||
float: left;
|
||||
position: relative;
|
||||
height: 160px;
|
||||
min-width: 350px;
|
||||
max-width: 500px;
|
||||
margin-right: 25px;
|
||||
margin-left: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.dashboard-stats-container {
|
||||
@@ -1443,9 +1448,6 @@ a:hover .dashboard-stats-square {
|
||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||
box-shadow: inset 0 0 0 2px #e9a049;
|
||||
}
|
||||
#dashboard-no-recently-added {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.dashboard-recent-media-row {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
@@ -1749,6 +1751,7 @@ a:hover .dashboard-recent-media-cover {
|
||||
box-shadow: inset 0 0 0 2px #e9a049;
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
z-index: 2;
|
||||
}
|
||||
.summary-poster-face-overlay span {
|
||||
display: block;
|
||||
@@ -1962,7 +1965,10 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
|
||||
.item-children-instance {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
.item-children-instance.max-height {
|
||||
max-height: 700px;
|
||||
}
|
||||
.item-children-instance li {
|
||||
float: left;
|
||||
@@ -2098,7 +2104,7 @@ a:hover .item-children-poster {
|
||||
}
|
||||
.item-children-list-item-title {
|
||||
display: inline-block;
|
||||
width: calc(100% - 110px);
|
||||
width: calc(100% - 125px);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -2108,11 +2114,19 @@ a:hover .item-children-poster {
|
||||
color: #777;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
width: 60px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.nav-list {
|
||||
float: left;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.nav-list.nav-pills > li > a {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#new_title h3 {
|
||||
color: #f9be03;
|
||||
color: #E5A00D;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
font-weight: bold;
|
||||
@@ -2184,32 +2198,17 @@ li.advanced-setting {
|
||||
.user-info-username {
|
||||
font-size: 24px;
|
||||
color: #eee;
|
||||
padding-top: 27px;
|
||||
padding-top: 15px;
|
||||
padding-left: 105px;
|
||||
}
|
||||
.user-info-nav {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.user-info-nav > .active > a {
|
||||
color: #cc7b19;
|
||||
padding-left: 105px;
|
||||
}
|
||||
.nav-tabs > .active > a:hover,
|
||||
.nav-tabs > .active > a:focus {
|
||||
color: #e9a049;
|
||||
}
|
||||
.user-info-nav a:hover {
|
||||
color: #e9a049;
|
||||
text-decoration: none;
|
||||
}
|
||||
.user-info-nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.user-info-nav li {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.user-overview-stats-wrapper {
|
||||
}
|
||||
.user-overview-stats-wrapper ul {
|
||||
@@ -2361,9 +2360,6 @@ a .library-user-instance-box:hover {
|
||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||
box-shadow: inset 0 0 0 2px #e9a049;
|
||||
}
|
||||
#watched-stats-days-selection label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.home-padded-header {
|
||||
margin: 25px 0;
|
||||
height: 34px;
|
||||
@@ -3296,23 +3292,36 @@ pre::-webkit-scrollbar-track {
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0,0,0,.15);
|
||||
}
|
||||
#currentActivity,
|
||||
#home-stats,
|
||||
#library-stats {
|
||||
margin-left: -25px;
|
||||
}
|
||||
#currentActivity > *,
|
||||
#home-stats > *,
|
||||
#library-stats > * {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
@media only screen
|
||||
and (min-device-width: 300px)
|
||||
and (max-device-width: 450px) {
|
||||
and (min-width: 300px)
|
||||
and (max-width: 450px) {
|
||||
.home-platforms-instance {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
.dashboard-activity-instance {
|
||||
width: 100%;
|
||||
}
|
||||
.dashboard-activity-instance,
|
||||
.dashboard-stats-instance {
|
||||
width: 100%;
|
||||
}
|
||||
#currentActivity,
|
||||
#home-stats,
|
||||
#library-stats {
|
||||
margin-right: 25px;
|
||||
}
|
||||
}
|
||||
@media only screen
|
||||
and (min-device-width: 300px)
|
||||
and (max-device-width: 740px) {
|
||||
and (min-width: 300px)
|
||||
and (max-width: 740px) {
|
||||
.header-bar {
|
||||
display: block;
|
||||
float: none !important;
|
||||
@@ -3339,8 +3348,8 @@ pre::-webkit-scrollbar-thumb {
|
||||
}
|
||||
}
|
||||
@media only screen
|
||||
and (min-device-width: 740px)
|
||||
and (max-device-width: 1024px) {
|
||||
and (min-width: 740px)
|
||||
and (max-width: 1024px) {
|
||||
.button-bar {
|
||||
float: right !important;
|
||||
}
|
||||
@@ -3454,10 +3463,6 @@ pre::-webkit-scrollbar-thumb {
|
||||
.activity-queue tr:nth-child(even) td {
|
||||
background-color: rgba(255,255,255,0.010);
|
||||
}
|
||||
#days-selection label,
|
||||
#months-selection label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-sortable {
|
||||
height: 36px;
|
||||
padding: 0 20px 0 0;
|
||||
@@ -3491,6 +3496,9 @@ pre::-webkit-scrollbar-thumb {
|
||||
.selectize-input input[type='text'] {
|
||||
height: 20px;
|
||||
}
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
.small-muted {
|
||||
font-size: small;
|
||||
color: #777;
|
||||
@@ -3713,6 +3721,20 @@ a:hover .overlay-refresh-image {
|
||||
a:hover .overlay-refresh-image:hover {
|
||||
opacity: .9;
|
||||
}
|
||||
.smart-playlist-image {
|
||||
float: left;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
background-color: #8e6191;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
width: 32px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
#ip_error, #isp_error {
|
||||
color: #aaa;
|
||||
display: none;
|
||||
@@ -3720,10 +3742,6 @@ a:hover .overlay-refresh-image:hover {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#plexpy-log-levels label,
|
||||
#plex-log-levels label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#plexpy-notifiers-table .friendly_name,
|
||||
#notifier-config-modal span.notifier_id,
|
||||
#plexpy-newsletters-table .friendly_name,
|
||||
@@ -3831,19 +3849,19 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-position: center !important;
|
||||
}
|
||||
.platform-android {
|
||||
background-color: #a4ca39;
|
||||
background-color: #3ddc84;
|
||||
background-image: url(../images/platforms/android.svg);
|
||||
}
|
||||
.platform-atv {
|
||||
background-color: #858487;
|
||||
background-color: #a2aaad;
|
||||
background-image: url(../images/platforms/atv.svg);
|
||||
}
|
||||
.platform-chrome {
|
||||
background-color: #ed5e50;
|
||||
background-color: #db4437;
|
||||
background-image: url(../images/platforms/chrome.svg);
|
||||
}
|
||||
.platform-chromecast {
|
||||
background-color: #10a4e8;
|
||||
background-color: #4285f4;
|
||||
background-image: url(../images/platforms/chromecast.svg);
|
||||
}
|
||||
.platform-default {
|
||||
@@ -3851,11 +3869,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/default.svg);
|
||||
}
|
||||
.platform-dlna {
|
||||
background-color: #0cb14b;
|
||||
background-color: #4ba32f;
|
||||
background-image: url(../images/platforms/dlna.svg);
|
||||
}
|
||||
.platform-firefox {
|
||||
background-color: #e67817;
|
||||
background-color: #ff7139;
|
||||
background-image: url(../images/platforms/firefox.svg);
|
||||
}
|
||||
.platform-gtv {
|
||||
@@ -3863,27 +3881,27 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/gtv.svg);
|
||||
}
|
||||
.platform-ie {
|
||||
background-color: #00599e;
|
||||
background-color: #18bcef;
|
||||
background-image: url(../images/platforms/ie.svg);
|
||||
}
|
||||
.platform-ios {
|
||||
background-color: #858487;
|
||||
background-color: #a2aaad;
|
||||
background-image: url(../images/platforms/ios.svg);
|
||||
}
|
||||
.platform-kodi {
|
||||
background-color: #31afe1;
|
||||
background-color: #30aada;
|
||||
background-image: url(../images/platforms/kodi.svg);
|
||||
}
|
||||
.platform-lg {
|
||||
background-color: #a50034;
|
||||
background-color: #990033;
|
||||
background-image: url(../images/platforms/lg.svg);
|
||||
}
|
||||
.platform-linux {
|
||||
background-color: #1793d0;
|
||||
background-color: #0099cc;
|
||||
background-image: url(../images/platforms/linux.svg);
|
||||
}
|
||||
.platform-macos {
|
||||
background-color: #858487;
|
||||
background-color: #a2aaad;
|
||||
background-image: url(../images/platforms/macos.svg);
|
||||
}
|
||||
.platform-msedge {
|
||||
@@ -3891,11 +3909,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/msedge.svg);
|
||||
}
|
||||
.platform-opera {
|
||||
background-color: #ff1b2d;
|
||||
background-color: #fa1e4e;
|
||||
background-image: url(../images/platforms/opera.svg);
|
||||
}
|
||||
.platform-playstation {
|
||||
background-color: #034da2;
|
||||
background-color: #003087;
|
||||
background-image: url(../images/platforms/playstation.svg);
|
||||
}
|
||||
.platform-plex {
|
||||
@@ -3907,11 +3925,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/plexamp.svg);
|
||||
}
|
||||
.platform-roku {
|
||||
background-color: #6d3c97;
|
||||
background-color: #673293;
|
||||
background-image: url(../images/platforms/roku.svg);
|
||||
}
|
||||
.platform-safari {
|
||||
background-color: #00a9ec;
|
||||
background-color: #00d3f9;
|
||||
background-image: url(../images/platforms/safari.svg);
|
||||
}
|
||||
.platform-samsung {
|
||||
@@ -3931,7 +3949,7 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/wiiu.svg);
|
||||
}
|
||||
.platform-windows {
|
||||
background-color: #2fc0f5;
|
||||
background-color: #0078d7;
|
||||
background-image: url(../images/platforms/windows.svg);
|
||||
}
|
||||
.platform-wp {
|
||||
@@ -3947,55 +3965,55 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/xbox.svg);
|
||||
}
|
||||
.platform-android-rgba {
|
||||
background-color: rgba(164, 202, 57, 0.40);
|
||||
background-color: rgba(61, 220, 132, 0.40);
|
||||
}
|
||||
.platform-atv-rgba {
|
||||
background-color: rgba(133, 132, 135, 0.40);
|
||||
background-color: rgba(162, 170, 173, 0.40);
|
||||
}
|
||||
.platform-chrome-rgba {
|
||||
background-color: rgba(237, 94, 80, 0.40);
|
||||
background-color: rgba(219, 68, 55, 0.40);
|
||||
}
|
||||
.platform-chromecast-rgba {
|
||||
background-color: rgba(16, 164, 232, 0.40);
|
||||
background-color: rgba(66, 133, 244, 0.40);
|
||||
}
|
||||
.platform-default-rgba {
|
||||
background-color: rgba(229, 160, 13, 0.40);
|
||||
}
|
||||
.platform-dlna-rgba {
|
||||
background-color: rgba(12, 177, 75, 0.40);
|
||||
background-color: rgba(75, 163, 47, 0.40);
|
||||
}
|
||||
.platform-firefox-rgba {
|
||||
background-color: rgba(230, 120, 23, 0.40);
|
||||
background-color: rgba(255, 113, 57, 0.40);
|
||||
}
|
||||
.platform-gtv-rgba {
|
||||
background-color: rgba(0, 139, 207, 0.40);
|
||||
}
|
||||
.platform-ie-rgba {
|
||||
background-color: rgba(0, 89, 158, 0.40);
|
||||
background-color: rgba(24, 188, 239, 0.40);
|
||||
}
|
||||
.platform-ios-rgba {
|
||||
background-color: rgba(133, 132, 135, 0.40);
|
||||
background-color: rgba(162, 170, 173, 0.40);
|
||||
}
|
||||
.platform-kodi-rgba {
|
||||
background-color: rgba(49, 175, 225, 0.40);
|
||||
background-color: rgba(48, 170, 218, 0.40);
|
||||
}
|
||||
.platform-lg-rgba {
|
||||
background-color: rgba(165, 0, 52, 0.40);
|
||||
background-color: rgba(153, 0, 51, 0.40);
|
||||
}
|
||||
.platform-linux-rgba {
|
||||
background-color: rgba(23, 147, 208, 0.40);
|
||||
background-color: rgba(0, 153, 204, 0.40);
|
||||
}
|
||||
.platform-macos-rgba {
|
||||
background-color: rgba(133, 132, 135, 0.40);
|
||||
background-color: rgba(162, 170, 173, 0.40);
|
||||
}
|
||||
.platform-msedge-rgba {
|
||||
background-color: rgba(0, 120, 215, 0.40);
|
||||
}
|
||||
.platform-opera-rgba {
|
||||
background-color: rgba(255, 27, 45, 0.40);
|
||||
background-color: rgba(250, 30, 78, 0.40);
|
||||
}
|
||||
.platform-playstation-rgba {
|
||||
background-color: rgba(3, 77, 162, 0.40);
|
||||
background-color: rgba(0, 48, 135, 0.40);
|
||||
}
|
||||
.platform-plex-rgba {
|
||||
background-color: rgba(229, 160, 13, 0.40);
|
||||
@@ -4004,10 +4022,10 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-color: rgba(229, 160, 13, 0.40);
|
||||
}
|
||||
.platform-roku-rgba {
|
||||
background-color: rgba(109, 60, 151, 0.40);
|
||||
background-color: rgba(103, 50, 147, 0.40);
|
||||
}
|
||||
.platform-safari-rgba {
|
||||
background-color: rgba(0, 169, 236, 0.40);
|
||||
background-color: rgba(0, 211, 249, 0.40);
|
||||
}
|
||||
.platform-samsung-rgba {
|
||||
background-color: rgba(3, 78, 162, 0.40);
|
||||
@@ -4022,7 +4040,7 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-color: rgba(3, 169, 244, 0.40);
|
||||
}
|
||||
.platform-windows-rgba {
|
||||
background-color: rgba(47, 192, 245, 0.40);
|
||||
background-color: rgba(0, 120, 215, 0.40);
|
||||
}
|
||||
.platform-wp-rgba {
|
||||
background-color: rgba(104, 33, 122, 0.40);
|
||||
@@ -4350,3 +4368,66 @@ a[data-tab-destination] {
|
||||
.news-body a:hover {
|
||||
color: #f9be03;
|
||||
}
|
||||
|
||||
a.donate-with-crypto,
|
||||
a.donate-with-crypto > span {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
clear: none;
|
||||
clip: auto;
|
||||
cursor: default;
|
||||
display: block;
|
||||
float: none;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
max-height: none;
|
||||
min-height: none;
|
||||
padding: 0;
|
||||
opacity: 1;
|
||||
text-shadow: none;
|
||||
vertical-align: baseline;
|
||||
visibility: visible;
|
||||
width: auto;
|
||||
}
|
||||
a.donate-with-crypto {
|
||||
user-select: none;
|
||||
user-drag: none;
|
||||
-webkit-user-drag: none;
|
||||
text-decoration: none;
|
||||
background: #1652f0 linear-gradient(#1652f0, #0655ab);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease-in-out, padding 0.2s;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
padding: 9px 15px 11px 15px;
|
||||
position: relative;
|
||||
min-width: 160px;
|
||||
}
|
||||
a.donate-with-crypto:hover {
|
||||
background: #1652f0;
|
||||
}
|
||||
a.donate-with-crypto > span {
|
||||
color: white;
|
||||
font: normal 500 14px/20px -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', 'San Francisco', 'Roboto', 'Segoe UI', 'Helvetica Neue', 'Lucida Grande', sans-serif;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15);
|
||||
white-space: nowrap;
|
||||
}
|
||||
a.donate-with-crypto::after {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
@@ -218,7 +218,7 @@ DOCUMENTATION :: END
|
||||
<div class="sub-heading">Container</div>
|
||||
<div class="sub-value" id="transcode_container-${sk}">
|
||||
% if data['stream_container_decision'] == 'transcode':
|
||||
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
|
||||
Converting (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
|
||||
% else:
|
||||
Direct Play (${data['stream_container'].upper()})
|
||||
% endif
|
||||
@@ -397,7 +397,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-wrapper">
|
||||
<a href="${user_href}" title="${data['friendly_name']}">
|
||||
<a href="${user_href}" title="${data['username']}">
|
||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
||||
</a>
|
||||
<div class="dashboard-activity-metadata-title-container">
|
||||
@@ -408,6 +408,10 @@ DOCUMENTATION :: END
|
||||
<i class="fa fa-fw fa-pause"></i>
|
||||
% elif data['state'] == 'buffering':
|
||||
<i class="fa fa-fw fa-spinner"></i>
|
||||
% elif data['state'] == 'error':
|
||||
<i class="fa fa-fw fa-exclamation-triangle"></i>
|
||||
% else:
|
||||
<i class="fa fa-fw fa-question-circle"></i>
|
||||
% endif
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-title">
|
||||
@@ -519,7 +523,7 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-user">
|
||||
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
||||
<a href="${user_href}" title="${data['username']}">${data['friendly_name']}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<%!
|
||||
from plexpy import helpers
|
||||
from jellypy import helpers
|
||||
%>
|
||||
|
||||
% if data != None:
|
||||
@@ -115,21 +115,13 @@ DOCUMENTATION :: END
|
||||
var msg = 'Are you REALLY sure you want to purge all history for the <strong>${data["section_name"]}</strong> library?<br>' +
|
||||
'This is permanent and cannot be undone!';
|
||||
var url = 'delete_all_library_history';
|
||||
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
|
||||
confirmAjaxCall(url, msg, { server_id: '${server_id}', section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$('#undelete-library').click(function () {
|
||||
var msg = 'Are you sure you want to undelete this user?';
|
||||
var msg = 'Are you sure you want to undelete this library?';
|
||||
var url = 'undelete_library';
|
||||
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
// Move #confirm-modal to parent container
|
||||
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) {
|
||||
$('#confirm-modal-purge').appendTo($('#edit-library-modal').parent());
|
||||
}
|
||||
$('#edit-library-modal > #confirm-modal-purge').remove();
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
@@ -27,7 +27,7 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<%!
|
||||
from plexpy import helpers
|
||||
from jellypy import helpers
|
||||
%>
|
||||
|
||||
% if data != None:
|
||||
@@ -134,13 +134,5 @@ DOCUMENTATION :: END
|
||||
var url = 'undelete_user';
|
||||
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
// Move #confirm-modal-purge to parent container
|
||||
if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) {
|
||||
$('#confirm-modal-purge').appendTo($('#edit-user-modal').parent());
|
||||
}
|
||||
$('#edit-user-modal > #confirm-modal-purge').remove();
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
289
data/interfaces/default/export_modal.html
Normal file
@@ -0,0 +1,289 @@
|
||||
<%doc>
|
||||
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
||||
|
||||
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
||||
|
||||
Filename: export_modal.html
|
||||
Version: 0.1
|
||||
Variable names: data [list]
|
||||
|
||||
data :: Usable parameters
|
||||
|
||||
== Global keys ==
|
||||
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
<%
|
||||
import jellypy
|
||||
from jellypy import exporter
|
||||
from jellypy.helpers import anon_url
|
||||
export = exporter.Export()
|
||||
thumb_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[0]])
|
||||
art_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[1]])
|
||||
%>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="info-modal-title">
|
||||
${title}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" class="form" id="export_metadata_form">
|
||||
<input type="hidden" id="export_section_id" name="export_section_id" value="${section_id or ''}" />
|
||||
<input type="hidden" id="export_user_id" name="export_user_id" value="${user_id or ''}" />
|
||||
<input type="hidden" id="export_rating_key" name="export_rating_key" value="${rating_key or ''}" />
|
||||
<input type="hidden" id="export_media_type" name="export_media_type" value="${media_type or ''}" />
|
||||
<input type="hidden" id="export_sub_media_type" name="export_sub_media_type" value="${sub_media_type or ''}" />
|
||||
<input type="hidden" id="export_export_type" name="export_export_type" value="${export_type or ''}" />
|
||||
<div class="form-group">
|
||||
<label>Instructions</label>
|
||||
<p class="help-block">
|
||||
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Exporter-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">Exporter Guide</a> for more details about each option.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_file_format">Data File Format</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="export_file_format" name="export_file_format">
|
||||
% for format in file_formats:
|
||||
<option value="${format}">${format.upper()}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Select the export data file format.</p>
|
||||
</div>
|
||||
% if not rating_key:
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="export_individual_files" name="export_individual_files" value="1"> Export Individual Files
|
||||
</label>
|
||||
<p class="help-block">Enable to export one file for each ${media_type} instead of a single file containing all ${media_type}s.</p>
|
||||
</div>
|
||||
% endif
|
||||
<div class="form-group">
|
||||
<label for="export_metadata_level">Metadata Export Level</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="export_metadata_level" name="export_metadata_level">
|
||||
<option value="0">Level 0 - None / Custom</option>
|
||||
<option value="1" selected>Level 1 - Basic Metadata</option>
|
||||
<option value="2">Level 2 - Extended Metadata</option>
|
||||
<option value="3">Level 3 - Advanced Metadata</option>
|
||||
<option value="9">Level 9 - All Metadata</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Select the metadata export level. Higher levels include all fields from the lower levels.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_custom_metadata_fields">Custom Metadata Fields</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="text" class="form-control" id="export_custom_metadata_fields" name="export_custom_metadata_fields" data-field_type="Metadata">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Add additional fields to the selected metadata export level.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_media_info_level">Media Info Export Level</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="export_media_info_level" name="export_media_info_level">
|
||||
<option value="0">Level 0 - None / Custom</option>
|
||||
<option value="1" selected>Level 1 - Basic Media Info</option>
|
||||
<option value="2">Level 2 - Extended Media Info</option>
|
||||
<option value="3">Level 3 - Advanced Media Info</option>
|
||||
<option value="9">Level 9 - All Media Info</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Select the media info export level. Higher levels include all fields from the lower levels.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_custom_media_info_fields">Custom Media Info Fields</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="text" class="form-control" id="export_custom_media_info_fields" name="export_custom_media_info_fields" data-field_type="Media Info">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Add additional fields to the selected media info export level.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_thumb_level">Poster and Cover Image Export Level</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="export_thumb_level" name="export_thumb_level">
|
||||
<option value="0" selected>Level 0 - None / Custom</option>
|
||||
<option value="1">Level 1 - Uploaded and Selected Posters and Covers Only</option>
|
||||
<option value="2">Level 2 - Selected and Locked Posters and Covers Only</option>
|
||||
<option value="9">Level 9 - All Selected Posters and Covers</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
Select the level to export poster and cover image files.<br>Note: Only applies to ${thumb_media_types}.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_art_level">Background Artwork Image Export Level</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="export_art_level" name="export_art_level">
|
||||
<option value="0" selected>Level 0 - None / Custom</option>
|
||||
<option value="1">Level 1 - Uploaded and Selected Artwork Only</option>
|
||||
<option value="2">Level 2 - Selected and Locked Artwork Only</option>
|
||||
<option value="9">Level 9 - All Selected Artwork</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
Select the level to export background artwork image files.<br>Note: Only applies to ${art_media_types}.
|
||||
</p>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
Warning: Exporting images may take a long time! Images will be saved to a folder alongside the data file.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div>
|
||||
<input type="button" class="btn btn-bright btn-ok" data-dismiss="modal" id="export_metadata" value="Export">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="${http_root}js/selectize.plugin.disable-options.js"></script>
|
||||
<script>
|
||||
$('#export_metadata_form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
})
|
||||
|
||||
var optgroups = (function () {
|
||||
var optgroups = [];
|
||||
for (var i = 0; i <= 9; i++) {
|
||||
optgroups.push({$order: i+1, value: i});
|
||||
}
|
||||
return optgroups
|
||||
})()
|
||||
|
||||
var $export_custom_fields = $('#export_custom_metadata_fields, #export_custom_media_info_fields').selectize({
|
||||
plugins: {
|
||||
'remove_button': {},
|
||||
'disable_options': {
|
||||
disableField: 'level'
|
||||
}
|
||||
},
|
||||
maxItems: null,
|
||||
valueField: 'field',
|
||||
labelField: 'field',
|
||||
sortField: 'field',
|
||||
searchField: ['field'],
|
||||
optgroupField: 'level',
|
||||
optgroups: optgroups,
|
||||
lockOptgroupOrder: true,
|
||||
render: {
|
||||
optgroup_header: function(data, escape) {
|
||||
return '<div class="optgroup-header">' + escape(this.$input.data('field_type') + ' Level: ' + data.value) + '</div>';
|
||||
},
|
||||
option: function (item, escape) {
|
||||
return '<div data-field="' + escape(item.field) + '" data-level="' + escape(item.level) + '">' + escape(item.field) +'</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
var export_custom_metadata_fields = $export_custom_fields[0].selectize;
|
||||
var export_custom_media_info_fields = $export_custom_fields[1].selectize;
|
||||
|
||||
function setDisabledFields() {
|
||||
var metadata_export_level = $('#export_metadata_level option:selected').val();
|
||||
var media_info_export_level = $('#export_media_info_level option:selected').val();
|
||||
export_custom_metadata_fields.setDisabledOptions([...Array(parseInt(metadata_export_level) + 1).keys()]);
|
||||
export_custom_media_info_fields.setDisabledOptions([...Array(parseInt(media_info_export_level) + 1).keys()]);
|
||||
}
|
||||
|
||||
$('#export_metadata_level, #export_media_info_level').on('change', setDisabledFields);
|
||||
|
||||
function getExportFields() {
|
||||
$.ajax({
|
||||
url: 'get_export_fields',
|
||||
async: true,
|
||||
data: {
|
||||
media_type: $('#export_media_type').val(),
|
||||
sub_media_type: $('#export_sub_media_type').val()
|
||||
},
|
||||
success: function (result) {
|
||||
if (result) {
|
||||
export_custom_metadata_fields.addOption(result.metadata_fields);
|
||||
export_custom_media_info_fields.addOption(result.media_info_fields);
|
||||
setDisabledFields();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
getExportFields();
|
||||
|
||||
$('#export_file_format').on('change', function() {
|
||||
if ($(this).val() === 'm3u8') {
|
||||
$('#export_metadata_level').prop('disabled', true);
|
||||
$('#export_media_info_level').prop('disabled', true);
|
||||
$("#export_thumb_level").prop('disabled', true);
|
||||
$("#export_art_level").prop('disabled', true);
|
||||
export_custom_metadata_fields.disable();
|
||||
export_custom_media_info_fields.disable();
|
||||
} else {
|
||||
$('#export_metadata_level').prop('disabled', false);
|
||||
$('#export_media_info_level').prop('disabled', false);
|
||||
$("#export_thumb_level").prop('disabled', false);
|
||||
$("#export_art_level").prop('disabled', false);
|
||||
export_custom_metadata_fields.enable();
|
||||
export_custom_media_info_fields.enable();
|
||||
}
|
||||
})
|
||||
|
||||
$("#export_metadata").click(function() {
|
||||
var section_id = $('#export_section_id').val();
|
||||
var user_id = $('#export_user_id').val();
|
||||
var rating_key = $('#export_rating_key').val();
|
||||
var metadata_export_level = $('#export_metadata_level option:selected').val();
|
||||
var media_info_export_level = $('#export_media_info_level option:selected').val();
|
||||
var file_format = $('#export_file_format option:selected').val();
|
||||
var thumb_level = $("#export_thumb_level option:selected").val();
|
||||
var art_level = $("#export_art_level option:selected").val();
|
||||
var custom_fields = [
|
||||
$('#export_custom_metadata_fields').val(),
|
||||
$('#export_custom_media_info_fields').val()
|
||||
].filter(Boolean).join(',');
|
||||
var export_type = $('#export_export_type').val()
|
||||
var individual_files = $('#export_individual_files').is(':checked')
|
||||
|
||||
$.ajax({
|
||||
url: 'export_metadata',
|
||||
data: {
|
||||
section_id: section_id,
|
||||
user_id: user_id,
|
||||
rating_key: rating_key,
|
||||
metadata_level: metadata_export_level,
|
||||
media_info_level: media_info_export_level,
|
||||
file_format: file_format,
|
||||
thumb_level: thumb_level,
|
||||
art_level: art_level,
|
||||
custom_fields: custom_fields,
|
||||
export_type: export_type,
|
||||
individual_files: individual_files
|
||||
},
|
||||
async: true,
|
||||
success: function (data) {
|
||||
if (data.result === 'success') {
|
||||
$("#nav-tabs-export").click();
|
||||
redrawExportTable();
|
||||
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
|
||||
} else {
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -40,14 +40,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<ul class="nav nav-pills" role="tablist" id="graph-tabs">
|
||||
<li role="presentation"><a href="#tabs-1" aria-controls="tabs-1" data-toggle="tab" role="tab">Plays by Period</a></li>
|
||||
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" data-toggle="tab" role="tab">Stream Info</a></li>
|
||||
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" data-toggle="tab" role="tab">Play Totals</a></li>
|
||||
<div class="table-card-back">
|
||||
<ul class="nav nav-list nav-pills" role="tablist" id="graph-tabs">
|
||||
<li role="presentation"><a id="nav-tabs-plays" href="#tabs-plays" aria-controls="tabs-plays" data-toggle="tab" role="tab">Plays by Period</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-stream" href="#tabs-stream" aria-controls="tabs-stream" data-toggle="tab" role="tab">Stream Info</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-total" href="#tabs-total" aria-controls="tabs-total" data-toggle="tab" role="tab">Play Totals</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-1">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-plays">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
|
||||
@@ -123,7 +123,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-2">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-stream">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4><i class="fa fa-video-camera"></i> Daily Stream type breakdown <small>Last <span class="days">30</span> days</small></h4>
|
||||
@@ -195,7 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-3">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-total">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4><i class="fa fa-calendar"></i> Plays by month <small>Last <span class="months">12</span> months</small></h4>
|
||||
@@ -225,8 +225,6 @@
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/moment-duration-format.js"></script>
|
||||
<script src="${http_root}js/highcharts/js/highcharts.js"></script>
|
||||
<script src="${http_root}js/jquery.dataTables.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
@@ -341,14 +339,29 @@
|
||||
var yaxis = getLocalStorage('graph_type', 'plays');
|
||||
var current_day_range = getLocalStorage('graph_days', 30);
|
||||
var current_month_range = getLocalStorage('graph_months', 12);
|
||||
var current_tab = '#' + getLocalStorage('graph_tab', 'tabs-1');
|
||||
var current_tab = '#' + getLocalStorage('graph_tab', 'tabs-plays');
|
||||
|
||||
// Update tab values from upgrading
|
||||
switch (current_tab) {
|
||||
case '#tabs-1':
|
||||
current_tab = '#tabs-plays'
|
||||
break
|
||||
case '#tabs-2':
|
||||
current_tab = '#tabs-stream'
|
||||
break
|
||||
case '#tabs-3':
|
||||
current_tab = '#tabs-total'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
$('#yaxis-' + yaxis).prop('checked', true);
|
||||
$('#yaxis-' + yaxis).closest('label').addClass('active');
|
||||
$('#graph-days').val(current_day_range);
|
||||
$('#graph-months').val(current_month_range);
|
||||
$('#graph-tabs a[href="' + current_tab + '"]').closest('li').addClass('active');
|
||||
$(current_tab).addClass('active');
|
||||
$('#nav-' + current_tab.replace('#', '')).tab('show').trigger('show.bs.tab');
|
||||
//$(current_tab).addClass('active');
|
||||
|
||||
|
||||
$('.days').html(current_day_range);
|
||||
@@ -469,7 +482,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$('#graph-tabs a[href="#tabs-1"]').tab('show')
|
||||
$('#nav-tabs-plays').tab('show');
|
||||
}
|
||||
|
||||
function loadGraphsTab2(time_range, yaxis) {
|
||||
@@ -562,7 +575,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$('#graph-tabs a[href="#tabs-2"]').tab('show')
|
||||
$('#nav-tabs-2').tab('show');
|
||||
}
|
||||
|
||||
function loadGraphsTab3(time_range, yaxis) {
|
||||
@@ -586,16 +599,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
$('#graph-tabs a[href="#tabs-3"]').tab('show')
|
||||
$('#nav-tabs-total').tab('show');
|
||||
}
|
||||
|
||||
// Set initial state
|
||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
|
||||
// Tab1 opened
|
||||
$('#graph-tabs a[href="#tabs-1"]').on('shown.bs.tab', function (e) {
|
||||
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
|
||||
e.preventDefault();
|
||||
current_tab = $(this).attr('href');
|
||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||
@@ -603,7 +616,7 @@
|
||||
});
|
||||
|
||||
// Tab2 opened
|
||||
$('#graph-tabs a[href="#tabs-2"]').on('shown.bs.tab', function (e) {
|
||||
$('#nav-tabs-stream').on('shown.bs.tab', function (e) {
|
||||
e.preventDefault();
|
||||
current_tab = $(this).attr('href');
|
||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||
@@ -611,7 +624,7 @@
|
||||
});
|
||||
|
||||
// Tab3 opened
|
||||
$('#graph-tabs a[href="#tabs-3"]').on('shown.bs.tab', function (e) {
|
||||
$('#nav-tabs-total').on('shown.bs.tab', function (e) {
|
||||
e.preventDefault();
|
||||
current_tab = $(this).attr('href');
|
||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||
@@ -624,8 +637,8 @@
|
||||
forceMinMax($(this));
|
||||
current_day_range = $(this).val();
|
||||
setLocalStorage('graph_days', current_day_range);
|
||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
$('.days').html(current_day_range);
|
||||
});
|
||||
|
||||
@@ -635,25 +648,25 @@
|
||||
forceMinMax($(this));
|
||||
current_month_range = $(this).val();
|
||||
setLocalStorage('graph_months', current_month_range);
|
||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
$('.months').html(current_month_range);
|
||||
});
|
||||
|
||||
// User changed
|
||||
$('#graph-user').on('change', function() {
|
||||
selected_user_id = $(this).val() || null;
|
||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
});
|
||||
|
||||
// Y-axis changed
|
||||
$('#yaxis-selection').on('change', function() {
|
||||
yaxis = $('input[name=yaxis-options]:checked', '#yaxis-selection').val();
|
||||
setLocalStorage('graph_type', yaxis);
|
||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
});
|
||||
|
||||
function setGraphFormat(type) {
|
||||
|
||||
@@ -117,7 +117,6 @@
|
||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
@@ -104,7 +104,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% elif stat_id == 'top_users':
|
||||
<% user_href = page('user', row0['user_id']) if row0['user_id'] else '#' %>
|
||||
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
|
||||
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['user']}" class="hidden-xs">
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
||||
</a>
|
||||
% elif stat_id == 'top_platforms':
|
||||
@@ -122,7 +122,7 @@ DOCUMENTATION :: END
|
||||
% elif stat_id.startswith('popular'):
|
||||
<span class="dashboard-stats-stats-units">users</span>
|
||||
% elif stat_id == 'last_watched':
|
||||
<span class="dashboard-stats-stats-units" id="last-watched-header-info">${row0['friendly_name']}</span>
|
||||
<span class="dashboard-stats-stats-units" id="last-watched-header-info" title="${row0['user']}">${row0['friendly_name']}</span>
|
||||
% elif stat_id == 'most_concurrent':
|
||||
<span class="dashboard-stats-stats-units" id="most-concurrent-header-info">streams</span>
|
||||
% endif
|
||||
@@ -134,7 +134,7 @@ DOCUMENTATION :: END
|
||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}"
|
||||
data-rating_key="${row.get('rating_key')}" data-guid="${row.get('guid')}" data-title="${row.get('title')}"
|
||||
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
||||
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
|
||||
data-user_id="${row.get('user_id')}" data-user="${row.get('user')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
|
||||
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}" data-live="${row.get('live')}">
|
||||
<div class="sub-list">${loop.index + 1}</div>
|
||||
<div class="sub-value">
|
||||
@@ -152,7 +152,7 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
% elif stat_id == 'top_users':
|
||||
<% user_href = page('user', row['user_id']) if row['user_id'] else '#' %>
|
||||
<a href="${user_href}" title="${row['friendly_name']}">
|
||||
<a href="${user_href}" title="${row['user']}">
|
||||
${row['friendly_name']}
|
||||
</a>
|
||||
% elif stat_id == 'top_platforms':
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -2,7 +2,7 @@
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="mstile-150x150.png?v=2.0.5"/>
|
||||
<square150x150logo src="mstile-150x150.png?v=2.6.0"/>
|
||||
<TileColor>#282a2d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
|
||||
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 971 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -6,12 +6,12 @@
|
||||
"scope": "../../",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-chrome-192x192.png?v=2.0.5",
|
||||
"src": "android-chrome-192x192.png?v=2.6.0",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-256x256.png?v=2.0.5",
|
||||
"src": "android-chrome-256x256.png?v=2.6.0",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 10 KiB |
@@ -1 +1,32 @@
|
||||
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M5695 6555 c-135 -34 -244 -94 -342 -189 -40 -39 -73 -76 -73 -83 0 -7 -4 -13 -10 -13 -14 0 -87 -156 -106 -225 -22 -83 -26 -234 -8 -320 17 -79 86 -230 133 -288 l30 -39 -48 -71 c-39 -57 -159 -228 -251 -357 -69 -97 -398 -564 -416 -590 -13 -19 -60 -87 -105 -150 -45 -63 -107 -151 -138 -195 -30 -44 -59 -84 -63 -90 -7 -9 -251 -354 -346 -490 -92 -131 -173 -245 -175 -245 -1 0 -34 9 -72 21 -130 38 -325 31 -454 -18 -168 -63 -313 -196 -385 -354 -39 -87 -65 -183 -68 -256 0 -24 -3 -43 -4 -43 -2 0 -43 46 -91 102 -49 57 -100 117 -115 133 -14 17 -128 149 -253 295 -125 146 -251 292 -279 324 -56 65 -77 89 -108 126 -58 68 -152 178 -172 200 -12 14 -50 57 -83 96 l-61 71 27 44 c58 93 91 217 92 342 2 161 -38 294 -125 412 -133 181 -316 279 -542 292 -470 27 -833 -434 -699 -887 74 -251 275 -437 530 -490 132 -28 334 -6 421 45 l42 24 173 -197 c96 -108 186 -210 200 -227 15 -16 163 -187 330 -380 458 -529 491 -567 526 -605 18 -19 31 -35 30 -36 -6 -5 -265 -161 -277 -167 -8 -4 -34 -20 -58 -35 -194 -124 -634 -382 -651 -382 -12 0 -46 20 -75 44 -60 49 -180 112 -242 127 -21 5 -48 12 -59 15 -11 4 -65 9 -121 11 -81 4 -117 1 -182 -15 -261 -66 -462 -270 -528 -537 -10 -40 -11 -217 -2 -258 5 -23 11 -51 14 -61 29 -145 147 -312 284 -403 123 -82 224 -114 370 -118 83 -3 124 2 240 29 36 9 133 57 187 94 60 41 111 91 153 152 14 19 28 37 32 40 19 15 71 140 89 217 17 73 20 107 16 198 -4 61 -7 121 -9 134 -3 28 -46 0 482 321 179 108 379 228 444 265 104 59 120 65 133 52 13 -13 12 -22 -10 -78 -49 -123 -58 -165 -62 -262 -7 -149 25 -286 89 -383 47 -72 91 -128 125 -158 19 -17 39 -36 45 -42 27 -25 136 -94 150 -94 8 0 17 -4 20 -9 3 -5 16 -11 28 -14 13 -3 50 -12 83 -21 74 -19 278 -15 345 7 198 65 358 196 435 358 16 34 20 36 49 28 17 -4 49 -10 71 -14 22 -3 99 -16 170 -30 72 -13 144 -26 160 -29 28 -5 101 -18 170 -31 17 -3 80 -14 140 -25 61 -11 124 -22 140 -25 17 -4 49 -9 72 -12 40 -5 42 -7 48 -47 14 -98 29 -147 73 -235 36 -75 61 -110 121 -171 154 -154 280 -210 480 -213 134 -2 180 5 273 40 212 83 371 262 427 481 24 93 25 255 2 342 -64 241 -245 428 -481 501 -62 18 -97 23 -200 22 -107 0 -136 -4 -205 -26 -44 -15 -109 -43 -145 -64 -83 -48 -208 -171 -250 -245 -17 -32 -35 -60 -38 -61 -4 -2 -46 4 -93 13 -48 10 -104 20 -125 23 -22 3 -46 8 -54 11 -8 3 -33 7 -55 10 -38 5 -58 9 -122 21 -16 3 -53 10 -83 15 -30 6 -66 12 -79 15 -13 2 -103 19 -200 36 -169 30 -207 42 -196 60 10 16 -28 155 -62 224 -19 39 -54 96 -78 127 l-45 58 40 52 c96 125 143 266 143 433 1 164 -27 263 -108 391 -19 30 -35 57 -35 61 0 3 31 49 69 102 57 81 450 638 625 889 28 40 62 88 76 107 14 18 194 274 400 568 291 414 379 534 393 531 10 -2 27 -6 37 -9 78 -25 240 -29 338 -9 433 87 677 573 489 974 -93 200 -255 332 -478 389 -87 22 -227 25 -304 6z"/></g></svg>
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="350.000000pt" height="350.000000pt" viewBox="0 0 350.000000 350.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,350.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1566 3489 c-433 -46 -867 -274 -1141 -601 -404 -481 -526 -1100
|
||||
-334 -1688 91 -278 283 -569 498 -756 676 -589 1646 -589 2322 0 215 187 407
|
||||
478 498 756 142 436 113 895 -84 1305 -320 666 -1027 1061 -1759 984z m1147
|
||||
-604 c87 -36 146 -118 154 -214 10 -111 -39 -203 -137 -254 -49 -26 -63 -28
|
||||
-131 -25 l-76 3 -109 -154 c-60 -85 -190 -269 -290 -409 l-181 -255 26 -46
|
||||
c22 -38 26 -59 26 -121 0 -63 -5 -84 -29 -132 -27 -54 -28 -59 -13 -76 22 -24
|
||||
47 -86 47 -117 0 -14 6 -28 13 -30 6 -3 91 -16 187 -30 157 -23 175 -24 183
|
||||
-10 38 68 115 118 199 130 103 15 220 -51 268 -151 26 -52 29 -154 6 -207 -19
|
||||
-48 -82 -114 -129 -138 -151 -77 -346 22 -373 189 -7 46 15 39 -222 74 -142
|
||||
20 -155 21 -163 6 -65 -116 -225 -163 -347 -102 -116 58 -167 187 -126 323 8
|
||||
29 13 55 11 57 -3 3 -65 -33 -138 -79 -74 -46 -162 -100 -196 -120 l-62 -38 6
|
||||
-47 c11 -100 -46 -207 -136 -254 -43 -23 -66 -28 -121 -28 -77 0 -124 16 -175
|
||||
62 -48 41 -76 99 -82 167 -7 72 9 129 50 183 85 112 256 132 372 44 l31 -24
|
||||
174 109 c96 60 180 111 185 113 6 2 -2 16 -16 32 -35 39 -412 468 -414 471 0
|
||||
1 -21 -5 -45 -13 -57 -20 -142 -14 -196 14 -162 84 -197 288 -71 419 102 108
|
||||
291 101 386 -14 62 -75 78 -185 40 -273 l-21 -49 23 -28 c13 -16 102 -118 198
|
||||
-227 l175 -198 20 61 c26 78 64 125 124 155 63 31 117 39 177 26 49 -11 51
|
||||
-11 72 17 21 26 533 749 548 773 4 6 -4 28 -17 48 -88 133 -44 307 94 376 61
|
||||
31 163 36 221 11z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 123 KiB |
BIN
data/interfaces/default/images/logo-flat-white.ico
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
data/interfaces/default/images/logo-flat-white.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,8 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<title>android</title>
|
||||
<path fill="#fff" d="M31.944 21.318c5.556 0 11.113 0 16.67 0 0.042 0 0.084-0 0.126 0.001 0.548 0.012 0.554 0.012 0.554 0.555 0.002 2.526 0.001 5.052 0.001 7.577 0 5.789 0.003 11.577-0.002 17.365-0.001 1.197-0.344 2.274-1.205 3.155-0.759 0.777-1.671 1.191-2.753 1.22-0.757 0.019-1.515 0.011-2.273 0.016-0.772 0.005-0.774 0.006-0.774 0.751-0.001 2.505-0.032 5.010 0.013 7.514 0.024 1.305-0.386 2.363-1.302 3.29-1.214 1.23-3.457 1.485-4.769 0.396-1.051-0.873-1.725-1.978-1.715-3.423 0.019-2.547 0.010-5.093 0.003-7.64-0.003-1.010 0.144-0.869-0.858-0.876-1.158-0.008-2.315-0.005-3.473-0.001-0.829 0.003-0.76-0.103-0.76 0.794-0.002 2.505-0.027 5.010 0.010 7.514 0.019 1.278-0.377 2.325-1.281 3.235-1.199 1.208-3.371 1.494-4.716 0.437-1.067-0.838-1.779-1.932-1.77-3.386 0.017-2.61 0.005-5.219 0.005-7.829 0-0.147-0.008-0.295 0-0.442 0.013-0.24-0.092-0.339-0.334-0.335-0.736 0.012-1.473 0.002-2.209 0.022-0.575 0.015-1.129-0.058-1.673-0.251-1.682-0.597-2.691-2.017-2.737-3.858-0.063-2.566-0.031-5.135-0.035-7.703-0.007-5.304-0.010-10.608-0.016-15.912-0.001-0.568-0.017-1.136-0.018-1.704-0-0.464 0.006-0.472 0.494-0.479 0.989-0.013 1.978-0.023 2.968-0.023 4.609-0.002 9.219-0.001 13.829-0.001-0.001 0.006-0.001 0.014-0.001 0.021z"></path>
|
||||
<path fill="#fff" d="M31.944 19.89c-5.535 0-11.071 0.002-16.606-0.002-0.717-0-0.772 0.153-0.687-0.747 0.189-2.003 0.58-3.948 1.437-5.784 1.041-2.228 2.47-4.152 4.433-5.648 0.864-0.658 1.646-1.43 2.624-1.932 0.216-0.111 0.25-0.23 0.129-0.443-0.363-0.64-0.715-1.286-1.059-1.937-0.441-0.835-0.877-1.674-1.302-2.518-0.247-0.491-0.206-0.765 0.103-0.941 0.342-0.194 0.625-0.077 0.892 0.415 0.721 1.329 1.429 2.664 2.142 3.997 0.069 0.13 0.141 0.258 0.215 0.386 0.226 0.39 0.228 0.394 0.671 0.218 2.478-0.987 5.051-1.43 7.715-1.338 2.143 0.074 4.214 0.501 6.214 1.273 0.118 0.045 0.241 0.081 0.35 0.142 0.186 0.102 0.303 0.067 0.405-0.126 0.534-1.023 1.075-2.043 1.617-3.062 0.297-0.557 0.592-1.115 0.908-1.66 0.189-0.325 0.514-0.408 0.809-0.253 0.292 0.153 0.366 0.43 0.175 0.817-0.39 0.79-0.791 1.575-1.204 2.353-0.383 0.725-0.789 1.438-1.18 2.159-0.19 0.351-0.181 0.348 0.158 0.573 1.666 1.102 3.266 2.297 4.577 3.814 1.895 2.192 3.115 4.723 3.574 7.598 0.119 0.746 0.175 1.503 0.266 2.254 0.038 0.311-0.097 0.421-0.393 0.394-0.146-0.014-0.295-0.002-0.442-0.002-5.514 0-11.028 0-16.543 0zM25.561 12.038c-0.063-1.117-0.623-1.553-1.433-1.566-0.833-0.014-1.419 0.462-1.455 1.603-0.025 0.776 0.66 1.407 1.463 1.409 0.79 0.001 1.421-0.64 1.424-1.445zM39.872 13.483c0.788-0.007 1.497-0.676 1.439-1.441-0.076-0.997-0.486-1.549-1.506-1.576-0.841-0.022-1.403 0.67-1.386 1.605 0.016 0.816 0.635 1.418 1.453 1.411z"></path>
|
||||
<path fill="#fff" d="M50.587 32.655c0-2.715-0.003-5.429 0.001-8.143 0.003-1.77 0.853-2.959 2.453-3.698 0.717-0.331 1.433-0.52 2.172-0.287 0.794 0.251 1.537 0.649 2.123 1.273 0.519 0.552 0.839 1.207 0.944 1.957 0.052 0.374 0.082 0.754 0.083 1.131 0.005 5.282-0.005 10.564 0.010 15.846 0.004 1.249-0.402 2.288-1.278 3.179-1.245 1.267-3.35 1.546-4.76 0.479-1.076-0.815-1.719-1.943-1.745-3.342-0.019-1.010-0.013-2.020-0.014-3.030-0.002-1.789-0.001-3.578-0.001-5.366 0.004-0 0.008-0 0.012-0z"></path>
|
||||
<path fill="#fff" d="M13.369 32.464c0 2.335-0.001 4.669 0.001 7.004 0 0.63 0.047 1.263 0.002 1.889-0.072 1.003-0.541 1.811-1.23 2.554-0.931 1.004-2.059 1.18-3.323 1.058-1.55-0.15-3.156-2.028-3.181-3.665-0.004-0.231-0.015-0.462-0.014-0.694 0.003-5.406 0.007-10.812 0.011-16.218 0.001-1.655 0.863-2.749 2.268-3.501 0.683-0.366 1.397-0.602 2.158-0.402 1.622 0.427 3.305 1.697 3.292 3.834-0.016 2.713-0.004 5.427-0.004 8.141 0.007-0 0.013-0 0.020 0z"></path>
|
||||
<path fill="#fff" d="M46.73 40.88c-0.003 0-0.007 0-0.010 0-1.475 0-2.67-1.195-2.67-2.67s1.195-2.67 2.67-2.67c1.475 0 2.67 1.195 2.67 2.67v0c0 0 0 0 0 0 0 1.471-1.19 2.664-2.659 2.67h-0.001zM17.27 40.88c-1.475 0-2.67-1.195-2.67-2.67s1.195-2.67 2.67-2.67c1.475 0 2.67 1.195 2.67 2.67v0c0 0.003 0 0.007 0 0.010 0 1.469-1.191 2.66-2.66 2.66-0.003 0-0.007 0-0.011 0h0.001zM47.68 24.83l5.32-9.23c0.095-0.159 0.151-0.351 0.151-0.557 0-0.405-0.219-0.76-0.546-0.951l-0.005-0.003c-0.16-0.095-0.354-0.152-0.56-0.152-0.407 0-0.764 0.22-0.957 0.547l-0.003 0.005-5.38 9.34c-4.027-1.851-8.738-2.93-13.7-2.93s-9.673 1.079-13.909 3.016l0.209-0.086-5.39-9.34c-0.204-0.28-0.531-0.46-0.9-0.46-0.613 0-1.11 0.497-1.11 1.11 0 0.167 0.037 0.325 0.103 0.467l-0.003-0.007 5.33 9.23c-9.153 5.047-15.453 14.286-16.323 25.059l-0.007 0.111h64c-0.875-10.883-7.171-20.121-16.158-25.088l-0.162-0.082z"></path>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -5,7 +5,7 @@
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
<% from plexpy import PLEX_SERVER_UP %>
|
||||
<% from jellypy import PLEX_SERVER_UP %>
|
||||
<div class="container-fluid">
|
||||
% for section in config['home_sections']:
|
||||
% if section == 'current_activity':
|
||||
@@ -24,17 +24,13 @@
|
||||
</div>
|
||||
<div id="currentActivity">
|
||||
% if PLEX_SERVER_UP:
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
|
||||
% elif not config['first_run_complete']:
|
||||
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
|
||||
% else:
|
||||
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
|
||||
% if _session['user_group'] == 'admin':
|
||||
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
|
||||
% endif
|
||||
</div>
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to the Plex server...</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +61,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="home-stats" class="home-platforms">
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +80,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="library-stats" class="library-platforms">
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,17 +128,12 @@
|
||||
<div class="col-md-12">
|
||||
<div id="recentlyAdded" style="margin-right: -15px;">
|
||||
% if PLEX_SERVER_UP:
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
|
||||
<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
<div class="text-muted">Plex Cloud server is sleeping.</div>
|
||||
% else:
|
||||
<div class="text-muted">There was an error communicating with your Plex Server.
|
||||
% if _session['user_group'] == 'admin':
|
||||
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
|
||||
% endif
|
||||
</div>
|
||||
<div id="dashboard-no-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to your Plex server...</div>
|
||||
% endif
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,6 +212,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% from plexpy.helpers import anon_url %>
|
||||
<div id="python2-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="python2-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||
<h4 class="modal-title">Unable to Update</h4>
|
||||
</div>
|
||||
<div class="modal-body" style="text-align: center;">
|
||||
<p>Tautulli is still running using Python 2 and cannot be updated past v2.6.3.</p>
|
||||
<p>Python 3 is required to continue receiving updates.</p>
|
||||
<p>
|
||||
<strong>Please see the <a href="${anon_url('https://github.com/Tautulli/Tautulli-Wiki/wiki/Upgrading-to-Python-3-%28Tautulli-v2.5%29')}" target="_blank" rel="noreferrer">wiki</a>
|
||||
for instructions on how to upgrade to Python 3.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||
@@ -229,8 +242,6 @@
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<% from plexpy import PLEX_SERVER_UP %>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
|
||||
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
||||
<script>
|
||||
@@ -260,8 +271,43 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
|
||||
% else:
|
||||
var msg_settings = '';
|
||||
% endif
|
||||
|
||||
var error_msg = 'There was an error communicating with your Plex Server.' + msg_settings;
|
||||
|
||||
% if 'current_activity' in config['home_sections'] or 'recently_added' in config['home_sections']:
|
||||
var server_status;
|
||||
server_status = setInterval(function() {
|
||||
$.getJSON('server_status', function (data) {
|
||||
if (data.connected === true) {
|
||||
clearInterval(server_status);
|
||||
% if 'current_activity' in config['home_sections']:
|
||||
$('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>');
|
||||
activityConnected();
|
||||
% endif
|
||||
% if 'recently_added' in config['home_sections']:
|
||||
$('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>');
|
||||
recentlyAddedConnected();
|
||||
% endif
|
||||
} else if (data.connected === false) {
|
||||
clearInterval(server_status);
|
||||
% if 'current_activity' in config['home_sections']:
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
||||
% endif
|
||||
% if 'recently_added' in config['home_sections']:
|
||||
$('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>');
|
||||
% endif
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
% endif
|
||||
</script>
|
||||
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
|
||||
% if 'current_activity' in config['home_sections']:
|
||||
<script>
|
||||
var defaultHandler = {
|
||||
get: function(target, name) {
|
||||
@@ -272,7 +318,7 @@
|
||||
var create_instances = [];
|
||||
var activity_ready = true;
|
||||
|
||||
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
|
||||
function getCurrentActivity() {
|
||||
activity_ready = false;
|
||||
@@ -298,13 +344,8 @@
|
||||
}
|
||||
|
||||
if (!(current_activity)) {
|
||||
% if _session['user_group'] == 'admin':
|
||||
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
|
||||
% else:
|
||||
var msg_settings = '';
|
||||
% endif
|
||||
$('#currentActivityHeader').hide();
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
||||
return
|
||||
}
|
||||
|
||||
@@ -377,6 +418,9 @@
|
||||
case 'buffering':
|
||||
state_icon = '<i class="fa fa-fw fa-spinner"></i> ';
|
||||
break;
|
||||
case 'error':
|
||||
state_icon = '<i class="fa fa-fw fa-exclamation-triangle"></i> ';
|
||||
break;
|
||||
default:
|
||||
state_icon = '<i class="fa fa-fw fa-question-circle"></i> ';
|
||||
}
|
||||
@@ -431,7 +475,7 @@
|
||||
|
||||
var transcode_container = '';
|
||||
if (s.stream_container_decision === 'transcode') {
|
||||
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
|
||||
transcode_container = 'Converting (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
|
||||
} else {
|
||||
transcode_container = 'Direct Play (' + s.stream_container.toUpperCase() + ')';
|
||||
}
|
||||
@@ -546,7 +590,7 @@
|
||||
}
|
||||
|
||||
// Update the progress bars, percent - 3 because of 3px padding-right
|
||||
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
|
||||
$('#buffer-bar-' + key).css({width: parseInt(s.transcode_progress) - 3 + '%'}).html(s.transcode_progress + '%')
|
||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||
if (s.live !== 1) {
|
||||
var progress_bar = $('#progress-bar-' + key);
|
||||
@@ -623,34 +667,36 @@
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentActivity();
|
||||
setInterval(function () {
|
||||
if (!(create_instances.length) && activity_ready) {
|
||||
getCurrentActivity();
|
||||
}
|
||||
}, ${config['home_refresh_interval'] * 1000});
|
||||
function activityConnected() {
|
||||
getCurrentActivity();
|
||||
setInterval(function () {
|
||||
if (!(create_instances.length) && activity_ready) {
|
||||
getCurrentActivity();
|
||||
}
|
||||
}, ${config['home_refresh_interval'] * 1000});
|
||||
|
||||
setInterval(function(){
|
||||
$('.progress_time_offset').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false);
|
||||
$(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration))
|
||||
}
|
||||
});
|
||||
$('.progress-bar').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var progress_percent = Math.floor(view_offset / stream_duration * 100);
|
||||
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
|
||||
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
|
||||
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
||||
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
setInterval(function(){
|
||||
$('.progress_time_offset').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false);
|
||||
$(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration))
|
||||
}
|
||||
});
|
||||
$('.progress-bar').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var progress_percent = Math.floor(view_offset / stream_duration * 100);
|
||||
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
|
||||
$(this).css({width: progress_percent - 3 + '%'}).html(progress_percent + '%')
|
||||
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
||||
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$('#currentActivity').on('click', '.external_ip-modal', function () {
|
||||
$.get('get_ip_address_details', {
|
||||
@@ -756,7 +802,7 @@
|
||||
if (user_id) {
|
||||
href = page('user', user_id);
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('user'));
|
||||
} else if (stat_id === 'top_platforms') {
|
||||
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
@@ -874,7 +920,7 @@
|
||||
getLibraryStats();
|
||||
</script>
|
||||
% endif
|
||||
% if 'recently_added' in config['home_sections'] and PLEX_SERVER_UP:
|
||||
% if 'recently_added' in config['home_sections']:
|
||||
<script>
|
||||
function recentlyAdded(recently_added_count, recently_added_type) {
|
||||
showMsg("Loading recently added items...", true, false, 0);
|
||||
@@ -902,7 +948,9 @@
|
||||
$('#recently-added-toggle-' + recently_added_type).closest('label').addClass('active');
|
||||
$('#recently-added-count').val(recently_added_count);
|
||||
|
||||
recentlyAdded(recently_added_count, recently_added_type);
|
||||
function recentlyAddedConnected() {
|
||||
recentlyAdded(recently_added_count, recently_added_type);
|
||||
}
|
||||
|
||||
function highlightAddedScrollerButton() {
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
@@ -994,4 +1042,16 @@
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% if _session['user_group'] == 'admin':
|
||||
<script>
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
if (urlParams.get('update') === 'python2') {
|
||||
$("#python2-modal").modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
});
|
||||
}
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
||||
@@ -39,9 +39,9 @@ DOCUMENTATION :: END
|
||||
from collections import defaultdict
|
||||
import re
|
||||
|
||||
from plexpy import notifiers
|
||||
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
|
||||
from plexpy.helpers import page, get_percent
|
||||
from jellypy import notifiers
|
||||
from jellypy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
|
||||
from jellypy.helpers import page, get_percent, cast_to_int
|
||||
|
||||
# Get audio codec file
|
||||
def af(codec):
|
||||
@@ -84,8 +84,10 @@ DOCUMENTATION :: END
|
||||
%>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
% if data['media_type'] not in ('photo_album', 'photo', 'playlist'):
|
||||
<% fallback = 'art-live-full' if data['live'] else None %>
|
||||
<div class="art-face" style="background-image:url(${page('pms_image_proxy', data['art'], data['rating_key'], 1920, 1080, fallback=fallback)})"></div>
|
||||
% endif
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -150,6 +152,29 @@ DOCUMENTATION :: END
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
|
||||
% elif data['media_type'] == 'photo_album':
|
||||
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
% if data['parent_title']:
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
% endif
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] in ('photo', 'clip'):
|
||||
<li class="hidden-xs hidden-sm"><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'playlist':
|
||||
% if user_info.get('user_id'):
|
||||
<li><a href="${page('user', user_info.get('user_id'))}">${user_info.get('friendly_name')}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
% elif data['section_id']:
|
||||
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
% endif
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
@@ -158,10 +183,13 @@ DOCUMENTATION :: END
|
||||
<div class="summary-content-title-wrapper">
|
||||
<div class="col-md-9">
|
||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||
% if data['media_type'] == 'track':
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
|
||||
<% legacy = '&legacy=1' if data['media_type'] in ('photo_album', 'photo', 'clip') else '' %>
|
||||
% if data['media_type'] in ('track', 'photo'):
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}${legacy}" target="_blank" rel="noreferrer" title="View on Plex Web">
|
||||
% elif data['media_type'] == 'playlist':
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/playlist?key=%2Fplaylists%2F${data['rating_key']}" target="_blank" rel="noreferrer" title="View on Plex Web">
|
||||
% elif not data['live']:
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}${legacy}" target="_blank" rel="noreferrer" title="View on Plex Web">
|
||||
% endif
|
||||
% if data['live']:
|
||||
<div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live')});">
|
||||
@@ -179,11 +207,14 @@ DOCUMENTATION :: END
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||
% elif data['media_type'] in ('artist', 'album', 'track', 'playlist', 'photo_album', 'photo', 'clip'):
|
||||
<div class="summary-poster-face-track" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 500, fallback='cover')});">
|
||||
<div class="summary-poster-face-overlay">
|
||||
<span></span>
|
||||
</div>
|
||||
% if data['media_type'] == 'playlist' and data['smart']:
|
||||
<span class="smart-playlist-image" title="Smart Playlist"><i class="fa fa-cog"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
@@ -214,7 +245,7 @@ DOCUMENTATION :: END
|
||||
<h3 class="hidden-xs">S${data['parent_media_index']} · E${data['media_index']}</h3>
|
||||
% endif
|
||||
% endif
|
||||
% elif data['media_type'] in ('movie', 'show', 'artist', 'collection'):
|
||||
% elif data['media_type'] in ('movie', 'show', 'artist', 'collection', 'playlist', 'photo_album'):
|
||||
<h1> </h1><h1>${data['title']}</h1>
|
||||
% elif data['media_type'] == 'season':
|
||||
<h1> </h1><h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
|
||||
@@ -230,26 +261,30 @@ DOCUMENTATION :: END
|
||||
<h1><a href="${page('info', data['grandparent_rating_key'])}">${data['original_title'] or data['grandparent_title']}</a></h1>
|
||||
<h2><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a> - ${data['title']}</h2>
|
||||
<h3 class="hidden-xs">T${data['media_index']}</h3>
|
||||
% elif data['media_type'] in ('photo', 'clip'):
|
||||
<h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
|
||||
<h2>${data['title']}</h2>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-content-wrapper">
|
||||
<div class="col-md-9">
|
||||
% if data['media_type'] == 'movie' or data['live']:
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
|
||||
% elif data['media_type'] in ('show', 'season', 'collection'):
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
|
||||
% elif data['media_type'] == 'episode':
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;">
|
||||
% elif data['media_type'] == 'artist' or data['media_type'] == 'album':
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;">
|
||||
% elif data['media_type'] == 'track':
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 180px;">
|
||||
% else:
|
||||
<div class="summary-content-padding hidden-xs hidden-sm">
|
||||
% endif
|
||||
% if data['media_type'] in ('movie', 'episode', 'track'):
|
||||
<%
|
||||
padding_height = ''
|
||||
if data['media_type'] == 'movie' or data['live']:
|
||||
padding_height = 'height: 305px;'
|
||||
elif data['media_type'] in ('show', 'season', 'collection'):
|
||||
padding_height = 'height: 270px;'
|
||||
elif data['media_type'] == 'episode':
|
||||
padding_height = 'height: 70px;'
|
||||
elif data['media_type'] in ('artist', 'album', 'playlist', 'photo_album', 'photo'):
|
||||
padding_height = 'height: 150px;'
|
||||
elif data['media_type'] in ('track', 'clip'):
|
||||
padding_height = 'height: 180px;'
|
||||
%>
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="${padding_height}">
|
||||
% if data['media_type'] in ('movie', 'episode', 'track', 'clip'):
|
||||
<div class="summary-content-media-info-wrapper">
|
||||
% if data['media_type'] != 'track' and media_info['video_codec']:
|
||||
<img class="summary-content-media-flag" title="${media_info['video_codec']}" src="${http_root}images/media_flags/video_codec/${media_info['video_codec'] | vf}.png" />
|
||||
@@ -268,16 +303,17 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-content-details-wrapper">
|
||||
% if data['rating']:
|
||||
% if data['rating_image']:
|
||||
% if data['rating_image'].startswith('imdb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
|
||||
<% rating = data['rating'] or data['audience_rating'] %>
|
||||
% if rating:
|
||||
% if data['audience_rating_image']:
|
||||
% if data['audience_rating_image'].startswith('imdb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||
<span class="rating-image rating-imdb"><strong>${rating}</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% if data['rating_image'].startswith('themoviedb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<span class="rating-image rating-themoviedb"><strong>${get_percent(data['rating'], 10)}%</strong></span>
|
||||
% if data['audience_rating_image'].startswith('themoviedb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||
<span class="rating-image rating-themoviedb"><strong>${get_percent(rating, 10)}%</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% if data['audience_rating_image'].startswith('rottentomatoes://'):
|
||||
@@ -291,11 +327,24 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% endif
|
||||
% else:
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<i class="star-icon fa fa-star"></i> <strong>${get_percent(data['rating'], 10)}%</strong>
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||
<i class="star-icon fa fa-star"></i> <strong>${get_percent(rating, 10)}%</strong>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="summary-content-details-tag">
|
||||
% if data['media_type'] in ('collection', 'playlist') and data['children_count']:
|
||||
<%
|
||||
if data['media_type'] == 'collection':
|
||||
suffix = MEDIA_TYPE_HEADERS[data['sub_media_type']]
|
||||
elif data['media_type'] == 'playlist':
|
||||
suffix = MEDIA_TYPE_HEADERS[data['playlist_type']]
|
||||
if data['children_count'] == 1:
|
||||
suffix = suffix[:-1]
|
||||
%>
|
||||
Items <strong> ${data['children_count']} ${suffix} </strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-details-tag">
|
||||
% if data['directors']:
|
||||
Directed by <strong> ${data['directors'][0]}</strong>
|
||||
@@ -315,6 +364,8 @@ DOCUMENTATION :: END
|
||||
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
||||
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||
Released <strong> ${data['year']}</strong>
|
||||
% elif data['media_type'] in ('photo', 'clip'):
|
||||
Taken <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
||||
% elif data['media_type'] == 'collection':
|
||||
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
|
||||
% elif data['year']:
|
||||
@@ -323,7 +374,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="summary-content-details-tag">
|
||||
% if data['duration']:
|
||||
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
|
||||
Runtime <strong> <span id="runtime">${data['duration']}</span></strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-details-tag">
|
||||
@@ -439,6 +490,17 @@ DOCUMENTATION :: END
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading track list...</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['media_type'] == 'photo_album':
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Photo List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading photo list...</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['media_type'] == 'collection':
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
@@ -447,30 +509,36 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading movies list...</div>
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading collection items...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collection-related-list-container" style="display: none;">
|
||||
</div>
|
||||
% endif
|
||||
% if data['media_type'] != 'collection':
|
||||
% elif data['media_type'] == 'playlist':
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
% if data['media_type'] in ('artist', 'album', 'track'):
|
||||
<span>Play History for <strong>${data['title']}</strong></span>
|
||||
% else:
|
||||
<span>Watch History for <strong>${data['title']}</strong></span>
|
||||
% endif
|
||||
<span>${MEDIA_TYPE_HEADERS[data['playlist_type']]} List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading playlist items...</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<%
|
||||
history_type = data['media_type'] in ('movie', 'show', 'season', 'episode', 'artist', 'album', 'track')
|
||||
history_active = 'active' if history_type else ''
|
||||
export_active = 'active' if not history_type else ''
|
||||
%>
|
||||
% if history_type and _session['user_group'] == 'admin':
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<ul class="nav nav-list nav-pills" role="tablist">
|
||||
<li class="${history_active}"><a id="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
<li class="${export_active}"><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
|
||||
</ul>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select rows to delete. Data is deleted upon exiting delete mode.</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||
<i class="fa fa-trash-o"></i> Delete mode
|
||||
</button>
|
||||
</div>
|
||||
% if source == 'history':
|
||||
<div class="btn-group">
|
||||
<a href="update_metadata?rating_key=${data['rating_key']}&update=True" class="btn btn-danger btn-edit" id="fix-metadata">
|
||||
@@ -510,37 +578,117 @@ DOCUMENTATION :: END
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display history_table" id="history_table-RK-${data['rating_key']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
<th align="left" id="date">Date</th>
|
||||
<th align="left" id="friendly_name">User</th>
|
||||
<th align="left" id="ip_address">IP Address</th>
|
||||
<th align="left" id="platform">Platform</th>
|
||||
<th align="left" id="product">Product</th>
|
||||
<th align="left" id="player">Player</th>
|
||||
<th align="left" id="title">Title</th>
|
||||
<th align="left" id="started">Started</th>
|
||||
<th align="left" id="paused_counter">Paused</th>
|
||||
<th align="left" id="stopped">Stopped</th>
|
||||
<th align="left" id="duration">Duration</th>
|
||||
<th align="left" id="percent_complete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div class="tab-content">
|
||||
% if history_type:
|
||||
<div role="tabpanel" class="tab-pane ${history_active}" id="tabs-history">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
% if data['media_type'] in ('artist', 'album', 'track'):
|
||||
<span>Play History for <strong>${data['title']}</strong></span>
|
||||
% else:
|
||||
<span>Watch History for <strong>${data['title']}</strong></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select rows to delete. Data is deleted upon exiting delete mode.</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||
<i class="fa fa-trash-o"></i> Delete mode
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-history"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display history_table" id="history_table-RK-${data['rating_key']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
<th align="left" id="date">Date</th>
|
||||
<th align="left" id="friendly_name">User</th>
|
||||
<th align="left" id="ip_address">IP Address</th>
|
||||
<th align="left" id="platform">Platform</th>
|
||||
<th align="left" id="product">Product</th>
|
||||
<th align="left" id="player">Player</th>
|
||||
<th align="left" id="title">Title</th>
|
||||
<th align="left" id="started">Started</th>
|
||||
<th align="left" id="paused_counter">Paused</th>
|
||||
<th align="left" id="stopped">Stopped</th>
|
||||
<th align="left" id="duration">Duration</th>
|
||||
<th align="left" id="percent_complete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
% if not data['live'] and _session['user_group'] == 'admin':
|
||||
<div role="tabpanel" class="tab-pane ${export_active}" id="tabs-export">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Metadata Exports for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
|
||||
data-section_id="${data['section_id']}" data-rating_key="${data['rating_key']}"
|
||||
data-media_type="${data['media_type']}" data-sub_media_type="${data['sub_media_type'] or data['playlist_type'] or ''}">
|
||||
<i class="fa fa-file-export"></i> Export metadata
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
|
||||
<i class="fa fa-refresh"></i> Refresh exports
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display export_table" id="export_table-RK-${data['rating_key']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="timestamp">Exported At</th>
|
||||
<th align="left" id="media_type_title">Media Type</th>
|
||||
<th align="left" id="rating_key">Rating Key</th>
|
||||
<th align="left" id="filename">Filename</th>
|
||||
<th align="left" id="file_format">File Format</th>
|
||||
<th align="left" id="metadata_level">Metadata Level</th>
|
||||
<th align="left" id="media_info_level">Media Info Level</th>
|
||||
<th align="left" id="media_info_level">Custom Fields</th>
|
||||
<th align="left" id="file_size">File Size</th>
|
||||
<th align="left" id="complete">Download</th>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -629,6 +777,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
@@ -636,13 +786,14 @@ DOCUMENTATION :: END
|
||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
|
||||
% if metadata:
|
||||
<%
|
||||
data = defaultdict(None, **metadata)
|
||||
history_user_id = '' if _session['user_group'] == 'admin' else _session['user_id']
|
||||
%>
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
|
||||
% if data['live']:
|
||||
<script>
|
||||
function get_history() {
|
||||
@@ -653,7 +804,7 @@ DOCUMENTATION :: END
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
guid: "${data['guid']}",
|
||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||
user_id: "${history_user_id}"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -669,7 +820,7 @@ DOCUMENTATION :: END
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
grandparent_rating_key: "${data['rating_key']}",
|
||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||
user_id: "${history_user_id}"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -685,7 +836,7 @@ DOCUMENTATION :: END
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
parent_rating_key: "${data['rating_key']}",
|
||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||
user_id: "${history_user_id}"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -701,96 +852,43 @@ DOCUMENTATION :: END
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
rating_key: "${data['rating_key']}",
|
||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||
user_id: "${history_user_id}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
% endif
|
||||
% if data['media_type'] != 'collection':
|
||||
% if data['media_type'] in ('movie', 'show', 'season', 'episode', 'artist', 'album', 'track'):
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
function loadHistoryTable() {
|
||||
get_history();
|
||||
history_table = $('#history_table-RK-${data["rating_key"]}').DataTable(history_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 12] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table-RK-${data["rating_key"]}', history_table);
|
||||
}
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
history_table.draw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
$(document).ready(function () {
|
||||
loadHistoryTable();
|
||||
});
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
// Send recently added notification
|
||||
$('#send-recently-added-notification').on('click', function () {
|
||||
var rating_key = $(this).data('id');
|
||||
|
||||
$('#send-recently-added-modal').modal();
|
||||
$('#send-recently-added-modal').one('click', '#confirm-send-notification', function () {
|
||||
$.ajax({
|
||||
url: 'send_manual_on_created',
|
||||
data: {
|
||||
rating_key: rating_key,
|
||||
notifier_id: $('#send-notification-notifier option:selected').val()
|
||||
},
|
||||
async: true,
|
||||
success: function (data) {
|
||||
if (data.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
|
||||
} else {
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
|
||||
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'photo_album', 'collection', 'playlist'):
|
||||
<script>
|
||||
$.ajax({
|
||||
url: 'get_item_children',
|
||||
type: 'GET',
|
||||
async: true,
|
||||
data: { rating_key : "${data['rating_key']}" },
|
||||
data: {
|
||||
rating_key: "${data['rating_key']}",
|
||||
media_type: "${data['media_type']}"
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#children-list").html(xhr.responseText);
|
||||
}
|
||||
@@ -804,7 +902,7 @@ DOCUMENTATION :: END
|
||||
type: 'GET',
|
||||
async: true,
|
||||
data: {
|
||||
rating_key : "${data['rating_key']}",
|
||||
rating_key: "${data['rating_key']}",
|
||||
title: "${data['title']}"
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
@@ -814,12 +912,29 @@ DOCUMENTATION :: END
|
||||
</script>
|
||||
% endif
|
||||
<script>
|
||||
$('.metadata-xml').on('tripleclick', function () {
|
||||
openPlexXML("/library/metadata/${data['rating_key']}");
|
||||
$(document).ready(function () {
|
||||
// Javascript to enable link to tab
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
if (hash) {
|
||||
$('.nav-list #nav-' + hash.replace('#' + prefix, "")).tab('show').trigger('show.bs.tab');
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
$('.nav-list a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||
});
|
||||
});
|
||||
|
||||
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
||||
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
var airdate = $("#airdate")
|
||||
var runtime = $("#runtime")
|
||||
airdate.html(moment(airdate.text()).format('MMM DD, YYYY'));
|
||||
runtime.html(humanDuration(runtime.text()));
|
||||
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
$('#channel-icon').popover({
|
||||
selector: '[data-toggle=popover]',
|
||||
@@ -833,6 +948,127 @@ DOCUMENTATION :: END
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<script>
|
||||
$("#toggle-export-modal").click(function() {
|
||||
$.ajax({
|
||||
url: 'export_metadata_modal',
|
||||
data: {
|
||||
section_id: $(this).data('section_id'),
|
||||
rating_key: $(this).data('rating_key'),
|
||||
media_type: $(this).data('media_type'),
|
||||
sub_media_type: $(this).data('sub_media_type')
|
||||
},
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#export-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadExportTable() {
|
||||
// Build export table
|
||||
export_table_options.ajax = {
|
||||
url: 'get_export_list',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
rating_key: "${data['rating_key']}"
|
||||
};
|
||||
}
|
||||
};
|
||||
export_table = $('#export_table-RK-${data["rating_key"]}').DataTable(export_table_options);
|
||||
export_table.columns([2, 7]).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-export');
|
||||
|
||||
clearSearchButton('export_table-RK-${data["rating_key"]}', export_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-export').on('shown.bs.tab', function() {
|
||||
if (typeof(export_table) === 'undefined') {
|
||||
loadExportTable();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
if (!($('#tabs-history').length)) {
|
||||
loadExportTable();
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-export-table").click(function () {
|
||||
export_table.draw();
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
history_table.draw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Send recently added notification
|
||||
$('#send-recently-added-notification').on('click', function () {
|
||||
var rating_key = $(this).data('id');
|
||||
|
||||
$('#send-recently-added-modal').modal();
|
||||
$('#send-recently-added-modal').one('click', '#confirm-send-notification', function () {
|
||||
$.ajax({
|
||||
url: 'send_manual_on_created',
|
||||
data: {
|
||||
rating_key: rating_key,
|
||||
notifier_id: $('#send-notification-notifier option:selected').val()
|
||||
},
|
||||
async: true,
|
||||
success: function (data) {
|
||||
if (data.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
|
||||
} else {
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('.metadata-xml').on('tripleclick', function () {
|
||||
openPlexXML("/library/metadata/${data['rating_key']}");
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% if data.get('poster_url'):
|
||||
<script>
|
||||
$('#hosted-poster').popover({
|
||||
|
||||
@@ -28,14 +28,15 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
from jellypy.helpers import cast_to_int, page
|
||||
%>
|
||||
% if data['children_count'] > 0:
|
||||
<div class="item-children-wrapper">
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
<% max_height ='max-height' if data['children_type'] in ('track', 'photo') or media_type == 'playlist' else '' %>
|
||||
<ul class="item-children-instance ${max_height} list-unstyled">
|
||||
% for child in data['children_list']:
|
||||
% if child['rating_key']:
|
||||
% if data['children_type'] == 'track':
|
||||
% if data['children_type'] in ('track', 'photo') or media_type == 'playlist':
|
||||
<li class="item-children-list-item">
|
||||
% else:
|
||||
<li>
|
||||
@@ -123,37 +124,144 @@ DOCUMENTATION :: END
|
||||
</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'track':
|
||||
% if loop.index % 2 == 0:
|
||||
<div class="item-children-list-item-even">
|
||||
<span class="item-children-list-item-index"> ${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
% if child['original_title']:
|
||||
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
|
||||
<div class="item-children-list-item-${e}">
|
||||
<span class="item-children-list-item-index">${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title">
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a>
|
||||
% if child['original_title']:
|
||||
<span class="text-muted"> - ${child['original_title']}</span>
|
||||
% endif
|
||||
% endif
|
||||
</span>
|
||||
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
||||
<% f = 'h:mm:ss' if cast_to_int(child['duration']) >= 3600000 else 'm:ss' %>
|
||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("${f}"));</script>
|
||||
</span>
|
||||
</div>
|
||||
% else:
|
||||
<div class="item-children-list-item-odd">
|
||||
<span class="item-children-list-item-index"> ${child['media_index']}</span>
|
||||
<span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
||||
% if child['original_title']:
|
||||
<span class="text-muted"> - ${child['original_title']}</span>
|
||||
% endif
|
||||
% elif data['children_type'] == 'photo':
|
||||
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
|
||||
<div class="item-children-list-item-${e}">
|
||||
<span class="item-children-list-item-index">${loop.index + 1}</span>
|
||||
<span class="item-children-list-item-title">
|
||||
% if child['media_type'] == 'photo_album':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-camera fa-fw"></i></span>
|
||||
% elif child['media_type'] == 'clip':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-video-camera fa-fw"></i></span>
|
||||
% else:
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>
|
||||
% endif
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
% if child['duration']:
|
||||
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
||||
<% f = 'h:mm:ss' if cast_to_int(child['duration']) >= 3600000 else 'm:ss' %>
|
||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("${f}"));</script>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
% elif media_type == 'playlist':
|
||||
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
|
||||
<div class="item-children-list-item-${e}">
|
||||
<span class="item-children-list-item-index">${loop.index + 1}</span>
|
||||
<span class="item-children-list-item-title">
|
||||
% if child['media_type'] == 'movie':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')}" data-height="120" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a>
|
||||
<span class="text-muted"> (${child['year']})</span>
|
||||
% elif child['media_type'] == 'episode':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>
|
||||
<a href="${page('info', child['grandparent_rating_key'])}" title="${child['grandparent_title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['grandparent_thumb'], child['grandparent_rating_key'], 300, 450, fallback='poster')}" data-height="120" data-width="80">
|
||||
${child['grandparent_title']}
|
||||
</span>
|
||||
</a> -
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 450, fallback='poster')}" data-height="120" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a>
|
||||
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">S${child['parent_media_index']}</a> · <a class="no-highlight" href="${page('info', child['rating_key'])}" title="${child['title']}">E${child['media_index']}</a>)</span>
|
||||
% elif child['media_type'] == 'track':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a> -
|
||||
<a href="${page('info', child['grandparent_rating_key'])}" title="${child['grandparent_title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['grandparent_thumb'], child['grandparent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['grandparent_title']}
|
||||
</span>
|
||||
</a>
|
||||
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>)</span>
|
||||
% elif child['media_type'] == 'photo':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a>
|
||||
% if child['grandparent_title']:
|
||||
- <a href="${page('info', child['grandparent_rating_key'])}" title="${child['grandparent_title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['grandparent_thumb'], child['grandparent_rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['grandparent_title']}
|
||||
</span>
|
||||
</a>
|
||||
% endif
|
||||
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>)</span>
|
||||
% elif child['media_type'] == 'clip':
|
||||
<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>
|
||||
<a href="${page('info', child['rating_key'])}" title="${child['title']}">
|
||||
<span class="thumb-tooltip" data-toggle="popover" data-img="${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')}" data-height="80" data-width="80">
|
||||
${child['title']}
|
||||
</span>
|
||||
</a>
|
||||
<span class="text-muted"> (<a class="no-highlight" href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>)</span>
|
||||
% endif
|
||||
</span>
|
||||
% if child['duration']:
|
||||
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||
<% f = 'h:mm:ss' if cast_to_int(child['duration']) >= 3600000 else 'm:ss' %>
|
||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("${f}"));</script>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
$('body').tooltip({
|
||||
selector: '[data-toggle="tooltip"]',
|
||||
container: 'body'
|
||||
});
|
||||
$('body').popover({
|
||||
selector: '[data-toggle="popover"]',
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||
content: function () {
|
||||
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.common import MEDIA_TYPE_HEADERS
|
||||
from plexpy.helpers import page
|
||||
from jellypy.common import MEDIA_TYPE_HEADERS
|
||||
from jellypy.helpers import page
|
||||
types = ('movie', 'show', 'artist', 'album')
|
||||
%>
|
||||
% for media_type in types:
|
||||
|
||||
@@ -54,7 +54,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
from jellypy.helpers import page
|
||||
%>
|
||||
% if data['results_count'] > 0:
|
||||
% if 'collection' in data['results_list'] and data['results_list']['collection']:
|
||||
@@ -65,7 +65,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['collection']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -90,7 +90,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['movie']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -115,7 +115,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['show']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -140,7 +140,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['season']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -165,7 +165,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['episode']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face episode-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 500, 280, fallback='art')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -191,7 +191,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['artist']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -215,7 +215,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['album']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -240,7 +240,7 @@ DOCUMENTATION :: END
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['track']:
|
||||
<li>
|
||||
<a href="${page('info', child['rating_key'])}" id="${child['rating_key']}">
|
||||
<a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')});">
|
||||
<div class="item-children-card-overlay">
|
||||
|
||||
8
data/interfaces/default/js/bootstrap.min.js
vendored
@@ -790,6 +790,9 @@ ColVis.prototype = {
|
||||
oStyle.top = oPos.top+"px";
|
||||
oStyle.left = iDivX+"px";
|
||||
|
||||
var iDocWidth = $(document).width();
|
||||
var iDocHeight = $(document).height();
|
||||
|
||||
document.body.appendChild( nBackground );
|
||||
document.body.appendChild( nHidden );
|
||||
document.body.appendChild( this.dom.catcher );
|
||||
@@ -819,12 +822,17 @@ ColVis.prototype = {
|
||||
|
||||
var iDivWidth = $(nHidden).outerWidth();
|
||||
var iDivHeight = $(nHidden).outerHeight();
|
||||
var iDocWidth = $(document).width();
|
||||
var iDivMarginTop = parseInt($(nHidden).css("marginTop"), 10);
|
||||
var iDivMarginBottom = parseInt($(nHidden).css("marginBottom"), 10);
|
||||
|
||||
if ( iLeft + iDivWidth > iDocWidth )
|
||||
{
|
||||
nHidden.style.left = (iDocWidth-iDivWidth)+"px";
|
||||
}
|
||||
if ( iDivY + iDivHeight > iDocHeight )
|
||||
{
|
||||
nHidden.style.top = (oPos.top - iDivHeight - iDivMarginTop - iDivMarginBottom)+"px";
|
||||
}
|
||||
}
|
||||
|
||||
this.s.hidden = false;
|
||||
@@ -846,7 +854,8 @@ ColVis.prototype = {
|
||||
this.s.hidden = true;
|
||||
|
||||
$(this.dom.collection).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {
|
||||
this.style.display = "none";
|
||||
// this.style.display = "none";
|
||||
document.body.removeChild( this );
|
||||
} );
|
||||
|
||||
$(this.dom.background).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {
|
||||
|
||||
2
data/interfaces/default/js/jquery-3.5.1.min.js
vendored
Normal file
1
data/interfaces/default/js/jquery.inputaffix.min.js
vendored
Normal file
@@ -1,482 +0,0 @@
|
||||
/*! Moment Duration Format v1.3.0
|
||||
* https://github.com/jsmreese/moment-duration-format
|
||||
* Date: 2014-07-15
|
||||
*
|
||||
* Duration format plugin function for the Moment.js library
|
||||
* http://momentjs.com/
|
||||
*
|
||||
* Copyright 2014 John Madhavan-Reese
|
||||
* Released under the MIT license
|
||||
*/
|
||||
|
||||
(function (root, undefined) {
|
||||
|
||||
// repeatZero(qty)
|
||||
// returns "0" repeated qty times
|
||||
function repeatZero(qty) {
|
||||
var result = "";
|
||||
|
||||
// exit early
|
||||
// if qty is 0 or a negative number
|
||||
// or doesn't coerce to an integer
|
||||
qty = parseInt(qty, 10);
|
||||
if (!qty || qty < 1) { return result; }
|
||||
|
||||
while (qty) {
|
||||
result += "0";
|
||||
qty -= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// padZero(str, len [, isRight])
|
||||
// pads a string with zeros up to a specified length
|
||||
// will not pad a string if its length is aready
|
||||
// greater than or equal to the specified length
|
||||
// default output pads with zeros on the left
|
||||
// set isRight to `true` to pad with zeros on the right
|
||||
function padZero(str, len, isRight) {
|
||||
if (str == null) { str = ""; }
|
||||
str = "" + str;
|
||||
|
||||
return (isRight ? str : "") + repeatZero(len - str.length) + (isRight ? "" : str);
|
||||
}
|
||||
|
||||
// isArray
|
||||
function isArray(array) {
|
||||
return Object.prototype.toString.call(array) === "[object Array]";
|
||||
}
|
||||
|
||||
// isObject
|
||||
function isObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||||
}
|
||||
|
||||
// findLast
|
||||
function findLast(array, callback) {
|
||||
var index = array.length;
|
||||
|
||||
while (index -= 1) {
|
||||
if (callback(array[index])) { return array[index]; }
|
||||
}
|
||||
}
|
||||
|
||||
// find
|
||||
function find(array, callback) {
|
||||
var index = 0,
|
||||
max = array.length,
|
||||
match;
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
match = callback;
|
||||
callback = function (item) {
|
||||
return item === match;
|
||||
};
|
||||
}
|
||||
|
||||
while (index < max) {
|
||||
if (callback(array[index])) { return array[index]; }
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// each
|
||||
function each(array, callback) {
|
||||
var index = 0,
|
||||
max = array.length;
|
||||
|
||||
if (!array || !max) { return; }
|
||||
|
||||
while (index < max) {
|
||||
if (callback(array[index], index) === false) { return; }
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// map
|
||||
function map(array, callback) {
|
||||
var index = 0,
|
||||
max = array.length,
|
||||
ret = [];
|
||||
|
||||
if (!array || !max) { return ret; }
|
||||
|
||||
while (index < max) {
|
||||
ret[index] = callback(array[index], index);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// pluck
|
||||
function pluck(array, prop) {
|
||||
return map(array, function (item) {
|
||||
return item[prop];
|
||||
});
|
||||
}
|
||||
|
||||
// compact
|
||||
function compact(array) {
|
||||
var ret = [];
|
||||
|
||||
each(array, function (item) {
|
||||
if (item) { ret.push(item); }
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// unique
|
||||
function unique(array) {
|
||||
var ret = [];
|
||||
|
||||
each(array, function (_a) {
|
||||
if (!find(ret, _a)) { ret.push(_a); }
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// intersection
|
||||
function intersection(a, b) {
|
||||
var ret = [];
|
||||
|
||||
each(a, function (_a) {
|
||||
each(b, function (_b) {
|
||||
if (_a === _b) { ret.push(_a); }
|
||||
});
|
||||
});
|
||||
|
||||
return unique(ret);
|
||||
}
|
||||
|
||||
// rest
|
||||
function rest(array, callback) {
|
||||
var ret = [];
|
||||
|
||||
each(array, function (item, index) {
|
||||
if (!callback(item)) {
|
||||
ret = array.slice(index);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// initial
|
||||
function initial(array, callback) {
|
||||
var reversed = array.slice().reverse();
|
||||
|
||||
return rest(reversed, callback).reverse();
|
||||
}
|
||||
|
||||
// extend
|
||||
function extend(a, b) {
|
||||
for (var key in b) {
|
||||
if (b.hasOwnProperty(key)) { a[key] = b[key]; }
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
// define internal moment reference
|
||||
var moment;
|
||||
|
||||
if (typeof require === "function") {
|
||||
try { moment = require('moment'); }
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
if (!moment && root.moment) {
|
||||
moment = root.moment;
|
||||
}
|
||||
|
||||
if (!moment) {
|
||||
throw "Moment Duration Format cannot find Moment.js";
|
||||
}
|
||||
|
||||
// moment.duration.format([template] [, precision] [, settings])
|
||||
moment.duration.fn.format = function () {
|
||||
|
||||
var tokenizer, tokens, types, typeMap, momentTypes, foundFirst, trimIndex,
|
||||
args = [].slice.call(arguments),
|
||||
settings = extend({}, this.format.defaults),
|
||||
// keep a shadow copy of this moment for calculating remainders
|
||||
remainder = moment.duration(this);
|
||||
|
||||
// add a reference to this duration object to the settings for use
|
||||
// in a template function
|
||||
settings.duration = this;
|
||||
|
||||
// parse arguments
|
||||
each(args, function (arg) {
|
||||
if (typeof arg === "string" || typeof arg === "function") {
|
||||
settings.template = arg;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof arg === "number") {
|
||||
settings.precision = arg;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isObject(arg)) {
|
||||
extend(settings, arg);
|
||||
}
|
||||
});
|
||||
|
||||
// types
|
||||
types = settings.types = (isArray(settings.types) ? settings.types : settings.types.split(" "));
|
||||
|
||||
// template
|
||||
if (typeof settings.template === "function") {
|
||||
settings.template = settings.template.apply(settings);
|
||||
}
|
||||
|
||||
// tokenizer regexp
|
||||
tokenizer = new RegExp(map(types, function (type) {
|
||||
return settings[type].source;
|
||||
}).join("|"), "g");
|
||||
|
||||
// token type map function
|
||||
typeMap = function (token) {
|
||||
return find(types, function (type) {
|
||||
return settings[type].test(token);
|
||||
});
|
||||
};
|
||||
|
||||
// tokens array
|
||||
tokens = map(settings.template.match(tokenizer), function (token, index) {
|
||||
var type = typeMap(token),
|
||||
length = token.length;
|
||||
|
||||
return {
|
||||
index: index,
|
||||
length: length,
|
||||
|
||||
// replace escaped tokens with the non-escaped token text
|
||||
token: (type === "escape" ? token.replace(settings.escape, "$1") : token),
|
||||
|
||||
// ignore type on non-moment tokens
|
||||
type: ((type === "escape" || type === "general") ? null : type)
|
||||
|
||||
// calculate base value for all moment tokens
|
||||
//baseValue: ((type === "escape" || type === "general") ? null : this.as(type))
|
||||
};
|
||||
}, this);
|
||||
|
||||
// unique moment token types in the template (in order of descending magnitude)
|
||||
momentTypes = intersection(types, unique(compact(pluck(tokens, "type"))));
|
||||
|
||||
// exit early if there are no momentTypes
|
||||
if (!momentTypes.length) {
|
||||
return pluck(tokens, "token").join("");
|
||||
}
|
||||
|
||||
// calculate values for each token type in the template
|
||||
each(momentTypes, function (momentType, index) {
|
||||
var value, wholeValue, decimalValue, isLeast, isMost;
|
||||
|
||||
// calculate integer and decimal value portions
|
||||
value = remainder.as(momentType);
|
||||
wholeValue = (value > 0 ? Math.floor(value) : Math.ceil(value));
|
||||
decimalValue = value - wholeValue;
|
||||
|
||||
// is this the least-significant moment token found?
|
||||
isLeast = ((index + 1) === momentTypes.length);
|
||||
|
||||
// is this the most-significant moment token found?
|
||||
isMost = (!index);
|
||||
|
||||
// update tokens array
|
||||
// using this algorithm to not assume anything about
|
||||
// the order or frequency of any tokens
|
||||
each(tokens, function (token) {
|
||||
if (token.type === momentType) {
|
||||
extend(token, {
|
||||
value: value,
|
||||
wholeValue: wholeValue,
|
||||
decimalValue: decimalValue,
|
||||
isLeast: isLeast,
|
||||
isMost: isMost
|
||||
});
|
||||
|
||||
if (isMost) {
|
||||
// note the length of the most-significant moment token:
|
||||
// if it is greater than one and forceLength is not set, default forceLength to `true`
|
||||
if (settings.forceLength == null && token.length > 1) {
|
||||
settings.forceLength = true;
|
||||
}
|
||||
|
||||
// rationale is this:
|
||||
// if the template is "h:mm:ss" and the moment value is 5 minutes, the user-friendly output is "5:00", not "05:00"
|
||||
// shouldn't pad the `minutes` token even though it has length of two
|
||||
// if the template is "hh:mm:ss", the user clearly wanted everything padded so we should output "05:00"
|
||||
// if the user wanted the full padded output, they can set `{ trim: false }` to get "00:05:00"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update remainder
|
||||
remainder.subtract(wholeValue, momentType);
|
||||
});
|
||||
|
||||
// trim tokens array
|
||||
if (settings.trim) {
|
||||
tokens = (settings.trim === "left" ? rest : initial)(tokens, function (token) {
|
||||
// return `true` if:
|
||||
// the token is not the least moment token (don't trim the least moment token)
|
||||
// the token is a moment token that does not have a value (don't trim moment tokens that have a whole value)
|
||||
return !(token.isLeast || (token.type != null && token.wholeValue));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// build output
|
||||
|
||||
// the first moment token can have special handling
|
||||
foundFirst = false;
|
||||
|
||||
// run the map in reverse order if trimming from the right
|
||||
if (settings.trim === "right") {
|
||||
tokens.reverse();
|
||||
}
|
||||
|
||||
tokens = map(tokens, function (token) {
|
||||
var val,
|
||||
decVal;
|
||||
|
||||
if (!token.type) {
|
||||
// if it is not a moment token, use the token as its own value
|
||||
return token.token;
|
||||
}
|
||||
|
||||
// apply negative precision formatting to the least-significant moment token
|
||||
if (token.isLeast && (settings.precision < 0)) {
|
||||
val = (Math.floor(token.wholeValue * Math.pow(10, settings.precision)) * Math.pow(10, -settings.precision)).toString();
|
||||
} else {
|
||||
val = token.wholeValue.toString();
|
||||
}
|
||||
|
||||
// remove negative sign from the beginning
|
||||
val = val.replace(/^\-/, "");
|
||||
|
||||
// apply token length formatting
|
||||
// special handling for the first moment token that is not the most significant in a trimmed template
|
||||
if (token.length > 1 && (foundFirst || token.isMost || settings.forceLength)) {
|
||||
val = padZero(val, token.length);
|
||||
}
|
||||
|
||||
// add decimal value if precision > 0
|
||||
if (token.isLeast && (settings.precision > 0)) {
|
||||
decVal = token.decimalValue.toString().replace(/^\-/, "").split(/\.|e\-/);
|
||||
switch (decVal.length) {
|
||||
case 1:
|
||||
val += "." + padZero(decVal[0], settings.precision, true).slice(0, settings.precision);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
val += "." + padZero(decVal[1], settings.precision, true).slice(0, settings.precision);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
val += "." + padZero(repeatZero((+decVal[2]) - 1) + (decVal[0] || "0") + decVal[1], settings.precision, true).slice(0, settings.precision);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw "Moment Duration Format: unable to parse token decimal value.";
|
||||
}
|
||||
}
|
||||
|
||||
// add a negative sign if the value is negative and token is most significant
|
||||
if (token.isMost && token.value < 0) {
|
||||
val = "-" + val;
|
||||
}
|
||||
|
||||
foundFirst = true;
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
// undo the reverse if trimming from the right
|
||||
if (settings.trim === "right") {
|
||||
tokens.reverse();
|
||||
}
|
||||
|
||||
return tokens.join("");
|
||||
};
|
||||
|
||||
moment.duration.fn.format.defaults = {
|
||||
// token definitions
|
||||
escape: /\[(.+?)\]/,
|
||||
years: /[Yy]+/,
|
||||
months: /M+/,
|
||||
weeks: /[Ww]+/,
|
||||
days: /[Dd]+/,
|
||||
hours: /[Hh]+/,
|
||||
minutes: /m+/,
|
||||
seconds: /s+/,
|
||||
milliseconds: /S+/,
|
||||
general: /.+?/,
|
||||
|
||||
// token type names
|
||||
// in order of descending magnitude
|
||||
// can be a space-separated token name list or an array of token names
|
||||
types: "escape years months weeks days hours minutes seconds milliseconds general",
|
||||
|
||||
// format options
|
||||
|
||||
// trim
|
||||
// "left" - template tokens are trimmed from the left until the first moment token that has a value >= 1
|
||||
// "right" - template tokens are trimmed from the right until the first moment token that has a value >= 1
|
||||
// (the final moment token is not trimmed, regardless of value)
|
||||
// `false` - template tokens are not trimmed
|
||||
trim: "left",
|
||||
|
||||
// precision
|
||||
// number of decimal digits to include after (to the right of) the decimal point (positive integer)
|
||||
// or the number of digits to truncate to 0 before (to the left of) the decimal point (negative integer)
|
||||
precision: 0,
|
||||
|
||||
// force first moment token with a value to render at full length even when template is trimmed and first moment token has length of 1
|
||||
forceLength: null,
|
||||
|
||||
// template used to format duration
|
||||
// may be a function or a string
|
||||
// template functions are executed with the `this` binding of the settings object
|
||||
// so that template strings may be dynamically generated based on the duration object
|
||||
// (accessible via `this.duration`)
|
||||
// or any of the other settings
|
||||
template: function () {
|
||||
var types = this.types,
|
||||
dur = this.duration,
|
||||
lastType = findLast(types, function (type) {
|
||||
return dur._data[type];
|
||||
});
|
||||
|
||||
// default template strings for each duration dimension type
|
||||
switch (lastType) {
|
||||
case "seconds":
|
||||
return "h:mm:ss";
|
||||
case "minutes":
|
||||
return "d[d] h:mm";
|
||||
case "hours":
|
||||
return "d[d] h[h]";
|
||||
case "days":
|
||||
return "M[m] d[d]";
|
||||
case "weeks":
|
||||
return "y[y] w[w]";
|
||||
case "months":
|
||||
return "y[y] M[m]";
|
||||
case "years":
|
||||
return "y[y]";
|
||||
default:
|
||||
return "y[y] M[m] d[d] h:mm:ss";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(this);
|
||||
11
data/interfaces/default/js/moment-duration-format.min.js
vendored
Normal file
2
data/interfaces/default/js/moment-with-locales.min.js
vendored
Normal file
@@ -330,31 +330,24 @@ function humanTime(seconds) {
|
||||
}
|
||||
}
|
||||
|
||||
function humanTimeClean(seconds) {
|
||||
var text;
|
||||
if (seconds >= 86400) {
|
||||
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + Math.floor(moment.duration((
|
||||
seconds % 86400), 'seconds').asHours()) + ' hrs ' + Math.floor(moment.duration(
|
||||
((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||
return text;
|
||||
} else if (seconds >= 3600) {
|
||||
text = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' + Math.floor(moment.duration(
|
||||
((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||
return text;
|
||||
} else if (seconds >= 60) {
|
||||
text = Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||
return text;
|
||||
} else {
|
||||
text = '0';
|
||||
return text;
|
||||
}
|
||||
}
|
||||
String.prototype.toProperCase = function () {
|
||||
return this.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
function getPercent(value1, value2) {
|
||||
value1 = parseFloat(value1) | 0
|
||||
value2 = parseFloat(value2) | 0
|
||||
|
||||
var percent = 0;
|
||||
if (value1 !== 0 && value2 !== 0) {
|
||||
percent = (value1 / value2) * 100
|
||||
}
|
||||
|
||||
return Math.round(percent)
|
||||
}
|
||||
|
||||
function millisecondsToMinutes(ms, roundToMinute) {
|
||||
if (ms > 0) {
|
||||
var minutes = Math.floor(ms / 60000);
|
||||
@@ -372,6 +365,61 @@ function millisecondsToMinutes(ms, roundToMinute) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function humanDuration(ms, sig='dhm', units='ms', return_seconds=300000) {
|
||||
var factors = {
|
||||
d: 86400000,
|
||||
h: 3600000,
|
||||
m: 60000,
|
||||
s: 1000,
|
||||
ms: 1
|
||||
}
|
||||
|
||||
ms = parseInt(ms);
|
||||
var d, h, m, s;
|
||||
|
||||
if (ms > 0) {
|
||||
if (return_seconds && ms < return_seconds) {
|
||||
sig = 'dhms'
|
||||
}
|
||||
|
||||
ms = ms * factors[units];
|
||||
|
||||
h = ms % factors['d'];
|
||||
d = Math.trunc(ms / factors['d']);
|
||||
|
||||
m = h % factors['h'];
|
||||
h = Math.trunc(h / factors['h']);
|
||||
|
||||
s = m % factors['m'];
|
||||
m = Math.trunc(m / factors['m']);
|
||||
|
||||
ms = s % factors['s'];
|
||||
s = Math.trunc(s / factors['s']);
|
||||
|
||||
var hd_list = [];
|
||||
if (sig >= 'd' && d > 0) {
|
||||
d = (sig === 'd' && h >= 12) ? d + 1 : d;
|
||||
hd_list.push(d.toString() + ' day' + ((d > 1) ? 's' : ''));
|
||||
}
|
||||
if (sig >= 'dh' && h > 0) {
|
||||
h = (sig === 'dh' && m >= 30) ? h + 1 : h;
|
||||
hd_list.push(h.toString() + ' hr' + ((h > 1) ? 's' : ''));
|
||||
}
|
||||
if (sig >= 'dhm' && m > 0) {
|
||||
m = (sig === 'dhm' && s >= 30) ? m + 1 : m;
|
||||
hd_list.push(m.toString() + ' min' + ((m > 1) ? 's' : ''));
|
||||
}
|
||||
if (sig >= 'dhms' && s > 0) {
|
||||
hd_list.push(s.toString() + ' sec' + ((s > 1) ? 's' : ''));
|
||||
}
|
||||
|
||||
return hd_list.join(' ')
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
}
|
||||
|
||||
// Our countdown plugin takes a callback, a duration, and an optional message
|
||||
$.fn.countdown = function (callback, duration, message) {
|
||||
// If no message is provided, we use an empty string
|
||||
@@ -555,16 +603,18 @@ function PopupCenter(url, title, w, h) {
|
||||
|
||||
|
||||
function setLocalStorage(key, value, path) {
|
||||
var key_path = key;
|
||||
if (path !== false) {
|
||||
key = key + '_' + window.location.pathname;
|
||||
key_path = key_path + '_' + window.location.pathname;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
localStorage.setItem(key_path, value);
|
||||
}
|
||||
function getLocalStorage(key, default_value, path) {
|
||||
var key_path = key;
|
||||
if (path !== false) {
|
||||
key = key + '_' + window.location.pathname;
|
||||
key_path = key_path + '_' + window.location.pathname;
|
||||
}
|
||||
var value = localStorage.getItem(key);
|
||||
var value = localStorage.getItem(key_path);
|
||||
if (value !== null) {
|
||||
return value
|
||||
} else if (default_value !== undefined) {
|
||||
@@ -803,3 +853,16 @@ function user_page(user_id, user) {
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
MEDIA_TYPE_HEADERS = {
|
||||
'movie': 'Movies',
|
||||
'show': 'TV Shows',
|
||||
'season': 'Seasons',
|
||||
'episode': 'Episodes',
|
||||
'artist': 'Artists',
|
||||
'album': 'Albums',
|
||||
'track': 'Tracks',
|
||||
'video': 'Videos',
|
||||
'audio': 'Tracks',
|
||||
'photo': 'Photos'
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Plugin: "disable_options" (selectize.js)
|
||||
* Copyright (c) 2013 Mondo Robot & contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
|
||||
* file except in compliance with the License. You may obtain a copy of the License at:
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
||||
* ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*
|
||||
* @authors Jake Myers <jmyers0022@gmail.com>, Vaughn Draughon <vaughn@rocksolidwebdesign.com>
|
||||
*/
|
||||
|
||||
Selectize.define('disable_options', function(options) {
|
||||
var self = this;
|
||||
|
||||
options = $.extend({
|
||||
'disableField': '',
|
||||
'disableOptions': []
|
||||
}, options);
|
||||
|
||||
self.refreshOptions = (function() {
|
||||
var original = self.refreshOptions;
|
||||
|
||||
return function() {
|
||||
original.apply(this, arguments);
|
||||
|
||||
$.each(options.disableOptions, function(index, option) {
|
||||
self.$dropdown_content.find('[data-' + options.disableField + '="' + String(option) + '"]').addClass('option-disabled');
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
self.onOptionSelect = (function() {
|
||||
var original = self.onOptionSelect;
|
||||
|
||||
return function(e) {
|
||||
var value, $target, $option;
|
||||
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
$target = $(e.currentTarget);
|
||||
|
||||
if ($target.hasClass('option-disabled')) {
|
||||
return;
|
||||
}
|
||||
return original.apply(this, arguments);
|
||||
};
|
||||
})();
|
||||
|
||||
self.disabledOptions = function() {
|
||||
return options.disableOptions;
|
||||
}
|
||||
|
||||
self.setDisabledOptions = function( values ) {
|
||||
options.disableOptions = values
|
||||
}
|
||||
|
||||
self.disableOptions = function( values ) {
|
||||
if ( ! ( values instanceof Array ) ) {
|
||||
values = [ values ]
|
||||
}
|
||||
values.forEach( function( val ) {
|
||||
if ( options.disableOptions.indexOf( val ) == -1 ) {
|
||||
options.disableOptions.push( val )
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
self.enableOptions = function( values ) {
|
||||
if ( ! ( values instanceof Array ) ) {
|
||||
values = [ values ]
|
||||
}
|
||||
values.forEach( function( val ) {
|
||||
var remove = options.disableOptions.indexOf( val );
|
||||
if ( remove + 1 ) {
|
||||
options.disableOptions.splice( remove, 1 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
});
|
||||
111
data/interfaces/default/js/tables/collections_table.js
Normal file
@@ -0,0 +1,111 @@
|
||||
collections_table_options = {
|
||||
"destroy": true,
|
||||
"language": {
|
||||
"search": "Search: ",
|
||||
"lengthMenu": "Show _MENU_ entries per page",
|
||||
"info": "Showing _START_ to _END_ of _TOTAL_ collections",
|
||||
"infoEmpty": "Showing 0 to 0 of 0 entries",
|
||||
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||
"emptyTable": "No data in table",
|
||||
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
||||
},
|
||||
"pagingType": "full_numbers",
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
"pageLength": 25,
|
||||
"order": [0, 'asc'],
|
||||
"autoWidth": false,
|
||||
"scrollX": true,
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"data": "titleSort",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['ratingKey'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<a href="' + page('info', rowData['ratingKey']) + '"><i class="fa fa-blank fa-fw"></i>' + thumb_popover + '</a>');
|
||||
}
|
||||
},
|
||||
"width": "50%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"data": "collectionMode",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var mode = '';
|
||||
if (cellData === -1) {
|
||||
mode = 'Library default';
|
||||
} else if (cellData === 0) {
|
||||
mode = 'Hide collection';
|
||||
} else if (cellData === 1) {
|
||||
mode = 'Hide items in this collection';
|
||||
} else if (cellData === 2) {
|
||||
mode = 'Show this collection and its items';
|
||||
}
|
||||
$(td).html(mode);
|
||||
}
|
||||
},
|
||||
"width": "20%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"data": "collectionSort",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var sort = '';
|
||||
if (cellData === 0) {
|
||||
sort = 'Release date';
|
||||
} else if (cellData === 1) {
|
||||
sort = 'Alphabetical';
|
||||
}
|
||||
$(td).html(sort);
|
||||
}
|
||||
},
|
||||
"width": "20%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"data": "childCount",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var type = MEDIA_TYPE_HEADERS[rowData['subtype']] || '';
|
||||
if (rowData['childCount'] == 1) {
|
||||
type = type.slice(0, -1);
|
||||
}
|
||||
$(td).html(cellData + ' ' + type);
|
||||
}
|
||||
},
|
||||
"width": "10%",
|
||||
"className": "no-wrap"
|
||||
}
|
||||
],
|
||||
"drawCallback": function (settings) {
|
||||
// Jump to top of page
|
||||
//$('html,body').scrollTop(0);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
|
||||
$('body').popover({
|
||||
selector: '[data-toggle="popover"]',
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||
content: function () {
|
||||
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
|
||||
}
|
||||
});
|
||||
},
|
||||
"preDrawCallback": function(settings) {
|
||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||
showMsg(msg, false, false, 0);
|
||||
},
|
||||
"rowCallback": function (row, rowData, rowIndex) {
|
||||
}
|
||||
};
|
||||
251
data/interfaces/default/js/tables/export_table.js
Normal file
@@ -0,0 +1,251 @@
|
||||
var date_format = 'YYYY-MM-DD';
|
||||
var time_format = 'hh:mm a';
|
||||
|
||||
$.ajax({
|
||||
url: 'get_date_formats',
|
||||
type: 'GET',
|
||||
success: function (data) {
|
||||
date_format = data.date_format;
|
||||
time_format = data.time_format;
|
||||
}
|
||||
});
|
||||
|
||||
export_table_options = {
|
||||
"destroy": true,
|
||||
"language": {
|
||||
"search": "Search: ",
|
||||
"lengthMenu": "Show _MENU_ entries per page",
|
||||
"info": "Showing _START_ to _END_ of _TOTAL_ export items",
|
||||
"infoEmpty": "Showing 0 to 0 of 0 entries",
|
||||
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||
"emptyTable": "No data in table",
|
||||
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
||||
},
|
||||
"pagingType": "full_numbers",
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
"pageLength": 25,
|
||||
"order": [0, 'desc'],
|
||||
"autoWidth": false,
|
||||
"scrollX": true,
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"data": "timestamp",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
$(td).html(moment(cellData, "X").format(date_format + ' ' + time_format));
|
||||
}
|
||||
},
|
||||
"width": "8%",
|
||||
"className": "no-wrap",
|
||||
"searchable": false
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"data": "media_type_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"width": "7%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"data": "rating_key",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null) {
|
||||
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
|
||||
}
|
||||
},
|
||||
"width": "6%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"data": "title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var tooltip;
|
||||
var filename;
|
||||
if (!rowData['individual_files']) {
|
||||
tooltip = '<span data-toggle="tooltip" title="Single File"><i class="fa fa-file-alt fa-fw"></i></span>';
|
||||
filename = cellData + '.' + rowData['file_format']
|
||||
} else {
|
||||
tooltip = '<span data-toggle="tooltip" title="Multiple Files"><i class="fa fa-folder fa-fw"></i></span>';
|
||||
filename = cellData
|
||||
}
|
||||
|
||||
if (rowData['complete'] === 1 && rowData['exists'] && !rowData['individual_files']) {
|
||||
$(td).html('<a href="view_export?export_id=' + rowData['export_id'] + '" target="_blank">' + tooltip + ' ' + filename + '</a>');
|
||||
} else {
|
||||
$(td).html(tooltip + ' ' + filename);
|
||||
}
|
||||
}
|
||||
},
|
||||
"width": "40%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"data": "file_format",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var images = '';
|
||||
if (rowData['thumb_level'] || rowData['art_level']) {
|
||||
images = ' + images';
|
||||
}
|
||||
$(td).html(cellData + images);
|
||||
}
|
||||
},
|
||||
"width": "7%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [5],
|
||||
"data": "metadata_level",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null) {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"width": "6%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [6],
|
||||
"data": "media_info_level",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null) {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"width": "6%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [7],
|
||||
"data": "custom_fields",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
$(td).html(cellData.replace(/,/g, ', '));
|
||||
}
|
||||
},
|
||||
"width": "6%",
|
||||
"className": "datatable-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [8],
|
||||
"data": "file_size",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '' && cellData !== null) {
|
||||
$(td).html(humanFileSize(cellData));
|
||||
}
|
||||
},
|
||||
"width": "6%",
|
||||
"className": "no-wrap",
|
||||
"searchable": false
|
||||
},
|
||||
{
|
||||
"targets": [9],
|
||||
"data": "complete",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData === 1 && rowData['exists']) {
|
||||
var tooltip_title = '';
|
||||
var icon = '';
|
||||
if (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) {
|
||||
tooltip_title = 'Zip Archive';
|
||||
icon = 'fa-file-archive';
|
||||
} else {
|
||||
tooltip_title = rowData['file_format'].toUpperCase() + ' File';
|
||||
icon = 'fa-file-download';
|
||||
}
|
||||
var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download';
|
||||
$(td).html('<button class="btn btn-xs btn-success pull-left" data-id="' + rowData['export_id'] + '"><span data-toggle="tooltip" data-placement="left" title="' + tooltip_title + '"><i class="fa ' + icon + ' fa-fw"></i> Download</span></button>');
|
||||
} else if (cellData === 0) {
|
||||
var percent = Math.min(getPercent(rowData['exported_items'], rowData['total_items']), 99)
|
||||
$(td).html('<span class="btn btn-xs btn-dark pull-left export-processing" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-spinner fa-spin fa-fw"></i> ' + percent + '%</span>');
|
||||
} else if (cellData === -1) {
|
||||
$(td).html('<span class="btn btn-xs btn-dark pull-left" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-exclamation-circle fa-fw"></i> Failed</span>');
|
||||
} else {
|
||||
$(td).html('<span class="btn btn-xs btn-dark pull-left" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-question-circle fa-fw"></i> Not Found</span>');
|
||||
}
|
||||
},
|
||||
"width": "7%",
|
||||
"className": "export_download",
|
||||
"searchable": false
|
||||
},
|
||||
{
|
||||
"targets": [10],
|
||||
"data": null,
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (rowData['complete'] !== 0) {
|
||||
$(td).html('<button class="btn btn-xs btn-danger pull-left" data-id="' + rowData['export_id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
|
||||
} else {
|
||||
$(td).html('<span class="btn btn-xs btn-danger pull-left" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-trash-o fa-fw"></i> Delete</span>');
|
||||
}
|
||||
},
|
||||
"width": "7%",
|
||||
"className": "export_delete",
|
||||
"searchable": false
|
||||
}
|
||||
],
|
||||
"drawCallback": function (settings) {
|
||||
// Jump to top of page
|
||||
//$('html,body').scrollTop(0);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
|
||||
// Create the tooltips.
|
||||
$('body').tooltip({
|
||||
selector: '[data-toggle="tooltip"]',
|
||||
container: 'body'
|
||||
});
|
||||
|
||||
if (export_processing_timer) {
|
||||
clearTimeout(export_processing_timer);
|
||||
}
|
||||
if ($('.export-processing').length) {
|
||||
export_processing_timer = setTimeout(redrawExportTable.bind(null, false), 2000);
|
||||
}
|
||||
},
|
||||
"preDrawCallback": function(settings) {
|
||||
if (!export_processing_timer) {
|
||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||
showMsg(msg, false, false, 0)
|
||||
}
|
||||
},
|
||||
"rowCallback": function (row, rowData, rowIndex) {
|
||||
if (rowData['complete'] === 0) {
|
||||
$(row).addClass('current-activity-row');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$('.export_table').on('click', '> tbody > tr > td.export_download > button', function (e) {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = export_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
e.preventDefault();
|
||||
window.location.href = 'download_export?export_id=' + rowData['export_id'];
|
||||
});
|
||||
|
||||
$('.export_table').on('click', '> tbody > tr > td.export_delete > button', function (e) {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = export_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var msg = 'Are you sure you want to delete the following export?<br /><br /><strong>' + rowData['title'] + '</strong>';
|
||||
var url = 'delete_export?export_id=' + rowData['export_id'];
|
||||
confirmAjaxCall(url, msg, null, null, redrawExportTable);
|
||||
});
|
||||
|
||||
function redrawExportTable(paging) {
|
||||
export_table.draw(paging);
|
||||
}
|
||||
|
||||
var export_processing_timer;
|
||||
@@ -55,13 +55,17 @@ history_table_options = {
|
||||
if (rowData['state'] !== null) {
|
||||
var state = '';
|
||||
if (rowData['state'] === 'playing') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Playing"><i class="fa fa-play fa-fw"></i></span>';
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Playing"><i class="fa fa-fw fa-play"></i></span>';
|
||||
} else if (rowData['state'] === 'paused') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-pause fa-fw"></i></span>';
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-fw fa-pause"></i></span>';
|
||||
} else if (rowData['state'] === 'buffering') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-spinner fa-fw"></i></span>';
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-fw fa-spinner"></i></span>';
|
||||
} else if (rowData['state'] === 'error') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Playback Error"><i class="fa fa-fw fa-exclamation-triangle"></i></span>';
|
||||
} else if (rowData['state'] === 'stopped') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-fw fa-stop"></i></span>';
|
||||
} else {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Unknown"><i class="fa fa-fw fa-question-circle"></i></span>';
|
||||
}
|
||||
$(td).html('<div><div style="float: left;">' + state + ' ' + date + '</div></div>');
|
||||
} else if (rowData['group_count'] > 1) {
|
||||
@@ -81,9 +85,9 @@ history_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['user_id']) {
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
|
||||
@@ -192,7 +192,7 @@ libraries_list_table_options = {
|
||||
"data": "duration",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html(humanTimeClean(cellData));
|
||||
$(td).html(humanDuration(cellData, 'dhm', 's'));
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
|
||||
@@ -107,15 +107,15 @@ media_info_table_options = {
|
||||
} else if (rowData['media_type'] === 'photo_album') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'photo') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, 'poster') + '" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'clip') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 500, 280, null, null, null, 'art') + '" data-height="80" data-width="140">' + rowData['title'] + '</span>';
|
||||
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
|
||||
100
data/interfaces/default/js/tables/playlists_table.js
Normal file
@@ -0,0 +1,100 @@
|
||||
playlists_table_options = {
|
||||
"destroy": true,
|
||||
"language": {
|
||||
"search": "Search: ",
|
||||
"lengthMenu": "Show _MENU_ entries per page",
|
||||
"info": "Showing _START_ to _END_ of _TOTAL_ playlists",
|
||||
"infoEmpty": "Showing 0 to 0 of 0 entries",
|
||||
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||
"emptyTable": "No data in table",
|
||||
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
||||
},
|
||||
"pagingType": "full_numbers",
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
"pageLength": 25,
|
||||
"order": [0, 'asc'],
|
||||
"autoWidth": false,
|
||||
"scrollX": true,
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"data": "title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var smart = '<i class="fa fa-blank fa-fw"></i>';
|
||||
if (rowData['smart']) {
|
||||
smart = '<span class="media-type-tooltip" data-toggle="tooltip" title="Smart Playlist"><i class="fa fa-cog fa-fw"></i></span> '
|
||||
}
|
||||
var breadcrumb = '';
|
||||
if (rowData['userID']) {
|
||||
breadcrumb = '&user_id=' + rowData['userID'];
|
||||
} else if (rowData['librarySectionID']) {
|
||||
breadcrumb = '§ion_id=' + rowData['librarySectionID'];
|
||||
}
|
||||
var thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['composite'], rowData['ratingKey'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + smart + cellData + '</span>';
|
||||
$(td).html('<a href="' + page('info', rowData['ratingKey']) + breadcrumb +'">' + thumb_popover + '</a>');
|
||||
}
|
||||
},
|
||||
"width": "60%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"data": "leafCount",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var type = MEDIA_TYPE_HEADERS[rowData['playlistType']] || '';
|
||||
if (rowData['leafCount'] === 1) {
|
||||
type = type.slice(0, -1);
|
||||
}
|
||||
$(td).html(cellData + ' ' + type);
|
||||
}
|
||||
},
|
||||
"width": "20%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"data": "duration",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
$(td).html(humanDuration(cellData, 'dhm'));
|
||||
}
|
||||
},
|
||||
"width": "20%",
|
||||
"className": "no-wrap"
|
||||
}
|
||||
],
|
||||
"drawCallback": function (settings) {
|
||||
// Jump to top of page
|
||||
//$('html,body').scrollTop(0);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
|
||||
// Create the tooltips.
|
||||
$('body').tooltip({
|
||||
selector: '[data-toggle="tooltip"]',
|
||||
container: 'body'
|
||||
});
|
||||
$('body').popover({
|
||||
selector: '[data-toggle="popover"]',
|
||||
html: true,
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
template: '<div class="popover history-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||
content: function () {
|
||||
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px; width: ' + $(this).data('width') + 'px;" />';
|
||||
}
|
||||
});
|
||||
},
|
||||
"preDrawCallback": function(settings) {
|
||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||
showMsg(msg, false, false, 0);
|
||||
$('[data-toggle="tooltip"]').tooltip('destroy');
|
||||
},
|
||||
"rowCallback": function (row, rowData, rowIndex) {
|
||||
}
|
||||
};
|
||||
@@ -51,9 +51,9 @@ sync_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['user_id']) {
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
|
||||
$(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
|
||||
@@ -62,9 +62,9 @@ users_list_table_options = {
|
||||
var inactive = '';
|
||||
if (!rowData['is_active']) { inactive = '<span class="inactive-user-tooltip" data-toggle="tooltip" title="User not on Plex server"><i class="fa fa-exclamation-triangle"></i></span>'; }
|
||||
if (cellData === '') {
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);">' + inactive + '</div></a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '"" title="' + rowData['username'] + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);">' + inactive + '</div></a>');
|
||||
} else {
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');">' + inactive + '</div></a>');
|
||||
$(td).html('<a href="' + page('user', rowData['user_id']) + '"" title="' + rowData['username'] + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');">' + inactive + '</div></a>');
|
||||
}
|
||||
},
|
||||
"orderable": false,
|
||||
@@ -78,7 +78,7 @@ users_list_table_options = {
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html('<div class="edit-user-name" data-id="' + rowData['row_id'] + '">' +
|
||||
'<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>' +
|
||||
'<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>' +
|
||||
'<input type="text" class="hidden" value="' + cellData + '">' +
|
||||
'</div>');
|
||||
} else {
|
||||
@@ -212,7 +212,7 @@ users_list_table_options = {
|
||||
"data": "duration",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html(humanTimeClean(cellData));
|
||||
$(td).html(humanDuration(cellData, 'dhm', 's'));
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
|
||||
@@ -79,7 +79,6 @@
|
||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/libraries.js${cache_param}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
@@ -36,8 +36,8 @@ DOCUMENTATION :: END
|
||||
<%def name="body()">
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.common import LIVE_TV_SECTION_ID
|
||||
from plexpy.helpers import page
|
||||
from jellypy.common import LIVE_TV_SECTION_ID
|
||||
from jellypy.helpers import page
|
||||
%>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
@@ -87,12 +87,19 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="user-info-nav">
|
||||
<ul class="user-info-nav" role="tablist">
|
||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<ul class="nav nav-list nav-pills" role="tablist">
|
||||
<li class="active"><a id="nav-tabs-profile" href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
||||
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<li><a id="nav-tabs-mediainfo" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
|
||||
% endif
|
||||
% if data['section_type'] != 'artist':
|
||||
<li><a id="nav-tabs-collections" href="#tabs-collections" role="tab" data-toggle="tab">Collections</a></li>
|
||||
% endif
|
||||
<li><a id="nav-tabs-playlists" href="#tabs-playlists" role="tab" data-toggle="tab">Playlists</a></li>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<li><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
|
||||
% endif
|
||||
% endif
|
||||
</ul>
|
||||
@@ -242,23 +249,22 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
% if config['get_file_sizes'] and data['section_id'] in config['get_file_sizes_hold']['section_ids']:
|
||||
<div id="get_file_sizes_message" style="text-align: center; margin-top: 20px;">
|
||||
% else:
|
||||
<div id="get_file_sizes_message" style="text-align: center; margin-top: 20px; display: none;">
|
||||
% endif
|
||||
<i class="fa fa-refresh fa-spin"></i> Tautulli is calculating the file sizes for the library's media info. This could take a few minutes depending on the size of your library.
|
||||
<br />
|
||||
You may leave this page and check back later.
|
||||
</div>
|
||||
% endif
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-history"></i> Media Info for <strong>
|
||||
<i class="fa fa-info-circle"></i> Media Info for <strong>
|
||||
<span class="set-username">${data['section_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
@@ -305,6 +311,157 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
% if data['section_type'] != 'artist':
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-collections">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-folder-open"></i> Collections for <strong>
|
||||
<span class="set-username">${data['section_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
|
||||
data-section_id="${data['section_id']}" data-media_type="collection" data-sub_media_type="${data['section_type']}"
|
||||
data-export_type="collection">
|
||||
<i class="fa fa-file-export"></i> Export collections
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-collections-table-button" id="refresh-collections-table">
|
||||
<i class="fa fa-refresh"></i> Refresh collections
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-collections"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display collections_table" id="collections_table-SID-${data['section_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="collectionTitle">Collection Title</th>
|
||||
<th align="left" id="collectionMode">Collection Mode</th>
|
||||
<th align="left" id="collectionSort">Collection Sort</th>
|
||||
<th align="left" id="collectionItems">Collection Items</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-playlists">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-list-alt"></i> Playlists for <strong>
|
||||
<span class="set-username">${data['section_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<% playlist_sub_media_type = {'movie': 'video', 'show': 'video', 'artist': 'audio', 'photo': 'photo'} %>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
|
||||
data-section_id="${data['section_id']}" data-media_type="playlist" data-sub_media_type="${playlist_sub_media_type.get(data['section_type'])}"
|
||||
data-export_type="playlist">
|
||||
<i class="fa fa-file-export"></i> Export playlists
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-playlists-table-button" id="refresh-playlists-table">
|
||||
<i class="fa fa-refresh"></i> Refresh playlists
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-playlists"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display playlists_table" id="playlists_table-SID-${data['section_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="playlistTitle">Playlist Title</th>
|
||||
<th align="left" id="playlistLeafCount">Playlist Items</th>
|
||||
<th align="left" id="playlistDuration">Playlist Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-export">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-file-export"></i> Metadata Exports for <strong>
|
||||
<span class="set-username">${data['section_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
|
||||
data-section_id="${data['section_id']}" data-media_type="${'photoalbum' if data['section_type'] == 'photo' else data['section_type']}"
|
||||
data-export_type="all">
|
||||
<i class="fa fa-file-export"></i> Export metadata
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
|
||||
<i class="fa fa-refresh"></i> Refresh exports
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display export_table" id="export_table-SID-${data['section_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="timestamp">Exported At</th>
|
||||
<th align="left" id="media_type_title">Media Type</th>
|
||||
<th align="left" id="rating_key">Rating Key</th>
|
||||
<th align="left" id="filename">Filename</th>
|
||||
<th align="left" id="file_format">File Format</th>
|
||||
<th align="left" id="metadata_level">Metadata Level</th>
|
||||
<th align="left" id="media_info_level">Media Info Level</th>
|
||||
<th align="left" id="media_info_level">Custom Fields</th>
|
||||
<th align="left" id="file_size">File Size</th>
|
||||
<th align="left" id="complete">Download</th>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,8 +492,7 @@ DOCUMENTATION :: END
|
||||
</%def>
|
||||
|
||||
<%def name="modalIncludes()">
|
||||
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog"
|
||||
aria-labelledby="edit-library-modal">
|
||||
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="edit-library-modal">
|
||||
</div>
|
||||
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||
</div>
|
||||
@@ -360,6 +516,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
@@ -369,6 +527,9 @@ DOCUMENTATION :: END
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
% if data:
|
||||
<% from plexpy.common import LIVE_TV_SECTION_ID %>
|
||||
<%
|
||||
history_user_id = '' if _session['user_group'] == 'admin' else _session['user_id']
|
||||
%>
|
||||
<script>
|
||||
% if str(data['section_id']).isdigit():
|
||||
var section_id = ${data['section_id']};
|
||||
@@ -384,14 +545,18 @@ DOCUMENTATION :: END
|
||||
var get_file_sizes = null;
|
||||
% endif
|
||||
</script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/collections_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/playlists_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
|
||||
<script>
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
$(".inactive-library-tooltip").tooltip();
|
||||
|
||||
function loadHistoryTable() {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
@@ -401,7 +566,7 @@ DOCUMENTATION :: END
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||
user_id: "${history_user_id}"
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -413,7 +578,7 @@ DOCUMENTATION :: END
|
||||
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
|
||||
}
|
||||
|
||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
||||
$('#nav-tabs-history').on('shown.bs.tab', function() {
|
||||
if (typeof(history_table) === 'undefined') {
|
||||
loadHistoryTable();
|
||||
}
|
||||
@@ -422,97 +587,70 @@ DOCUMENTATION :: END
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
$(".inactive-library-tooltip").tooltip();
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
function loadMediaInfoTable() {
|
||||
// Build media info table
|
||||
media_info_table_options.ajax = {
|
||||
url: 'get_library_media_info',
|
||||
</script>
|
||||
% if data['section_type'] != 'artist':
|
||||
<script>
|
||||
function loadCollectionsTable() {
|
||||
// Build collections table
|
||||
collections_table_options.ajax = {
|
||||
url: 'get_collections_list',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
refresh: refresh_table
|
||||
section_id: section_id
|
||||
};
|
||||
}
|
||||
};
|
||||
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
||||
collections_table = $('#collections_table-SID-${data["section_id"]}').DataTable(collections_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-media-info');
|
||||
var colvis = new $.fn.dataTable.ColVis(collections_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-collections');
|
||||
|
||||
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
|
||||
clearSearchButton('collections_table-SID-${data["section_id"]}', collections_table);
|
||||
}
|
||||
|
||||
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
|
||||
if (typeof(media_info_table) === 'undefined') {
|
||||
loadMediaInfoTable();
|
||||
$('#nav-tabs-collections').on('shown.bs.tab', function() {
|
||||
if (typeof(collections_table) === 'undefined') {
|
||||
loadCollectionsTable();
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-media-info-table").click(function () {
|
||||
media_info_child_table = {};
|
||||
refresh_table = true;
|
||||
refresh_child_tables = true;
|
||||
media_info_table.draw();
|
||||
refresh_table = false;
|
||||
$("#refresh-collections-table").click(function () {
|
||||
collections_table.draw();
|
||||
});
|
||||
|
||||
$("#edit-library-tooltip").tooltip();
|
||||
|
||||
// Load edit library modal
|
||||
$("#toggle-edit-library-modal").click(function() {
|
||||
$("#edit-library-tooltip").tooltip('hide');
|
||||
$.ajax({
|
||||
url: 'edit_library_dialog',
|
||||
data: { section_id: section_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#edit-library-modal").html(xhr.responseText);
|
||||
</script>
|
||||
% endif
|
||||
<script>
|
||||
function loadPlaylistsTable() {
|
||||
// Build playlists table
|
||||
playlists_table_options.ajax = {
|
||||
url: 'get_playlists_list',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
playlists_table = $('#playlists_table-SID-${data["section_id"]}').DataTable(playlists_table_options);
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
var colvis = new $.fn.dataTable.ColVis(playlists_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-playlists');
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
history_table.draw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
clearSearchButton('playlists_table-SID-${data["section_id"]}', playlists_table);
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
$('#nav-tabs-playlists').on('shown.bs.tab', function() {
|
||||
if (typeof(playlists_table) === 'undefined') {
|
||||
loadPlaylistsTable();
|
||||
}
|
||||
});
|
||||
% endif
|
||||
|
||||
$("#refresh-playlists-table").click(function () {
|
||||
playlists_table.draw();
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
@@ -634,11 +772,11 @@ DOCUMENTATION :: END
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
if (hash) {
|
||||
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
|
||||
$('.nav-list #nav-' + hash.replace('#' + prefix, "")).tab('show').trigger('show.bs.tab');
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
$('.user-info-nav a').on('shown.bs.tab', function (e) {
|
||||
$('.nav-list a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||
});
|
||||
|
||||
@@ -664,5 +802,143 @@ DOCUMENTATION :: END
|
||||
|
||||
});
|
||||
</script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<script>
|
||||
function loadMediaInfoTable() {
|
||||
// Build media info table
|
||||
media_info_table_options.ajax = {
|
||||
url: 'get_library_media_info',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
refresh: refresh_table
|
||||
};
|
||||
}
|
||||
};
|
||||
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-media-info');
|
||||
|
||||
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-mediainfo').on('shown.bs.tab', function() {
|
||||
if (typeof(media_info_table) === 'undefined') {
|
||||
loadMediaInfoTable();
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-media-info-table").click(function () {
|
||||
media_info_child_table = {};
|
||||
refresh_table = true;
|
||||
refresh_child_tables = true;
|
||||
media_info_table.draw();
|
||||
refresh_table = false;
|
||||
});
|
||||
|
||||
function loadExportTable() {
|
||||
// Build export table
|
||||
export_table_options.ajax = {
|
||||
url: 'get_export_list',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id
|
||||
};
|
||||
}
|
||||
};
|
||||
export_table = $('#export_table-SID-${data["section_id"]}').DataTable(export_table_options);
|
||||
export_table.columns([7]).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-export');
|
||||
|
||||
clearSearchButton('export_table-SID-${data["section_id"]}', export_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-export').on('shown.bs.tab', function() {
|
||||
if (typeof(export_table) === 'undefined') {
|
||||
loadExportTable();
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-export-table").click(function () {
|
||||
export_table.draw();
|
||||
});
|
||||
|
||||
$("#edit-library-tooltip").tooltip();
|
||||
|
||||
// Load edit library modal
|
||||
$("#toggle-edit-library-modal").click(function() {
|
||||
$("#edit-library-tooltip").tooltip('hide');
|
||||
$.ajax({
|
||||
url: 'edit_library_dialog',
|
||||
data: { section_id: section_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#edit-library-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".export-button").click(function() {
|
||||
$.ajax({
|
||||
url: 'export_metadata_modal',
|
||||
data: {
|
||||
section_id: $(this).data('section_id'),
|
||||
media_type: $(this).data('media_type'),
|
||||
sub_media_type: $(this).data('sub_media_type'),
|
||||
export_type: $(this).data('export_type')
|
||||
},
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#export-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_ids: history_to_delete.join(',') },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
history_table.draw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% endif
|
||||
</%def>
|
||||
@@ -32,7 +32,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
from jellypy.helpers import page
|
||||
%>
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-added-row-scroller" style="left: 0;">
|
||||
|
||||
@@ -25,7 +25,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
from jellypy.helpers import page
|
||||
|
||||
types = ('movie', 'show', 'artist', 'photo')
|
||||
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
|
||||
|
||||
@@ -19,17 +19,17 @@ DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
% if data:
|
||||
<% from plexpy.helpers import page %>
|
||||
<% from jellypy.helpers import page %>
|
||||
% for a in data:
|
||||
<ul class="list-unstyled">
|
||||
<div class="user-player-instance">
|
||||
<li>
|
||||
% if a['user_id']:
|
||||
<a href="${page('user', a['user_id'])}" title="${a['friendly_name']}">
|
||||
<a href="${page('user', a['user_id'])}" title="${a['username']}">
|
||||
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>
|
||||
</a>
|
||||
<div class=" user-player-instance-name">
|
||||
<a href="${page('user', a['user_id'])}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||
<a href="${page('user', a['user_id'])}" title="${a['username']}">${a['friendly_name']}</a>
|
||||
</div>
|
||||
% else:
|
||||
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%
|
||||
import plexpy
|
||||
plex_login = plexpy.CONFIG.HTTP_PLEX_ADMIN or plexpy.CONFIG.ALLOW_GUEST_ACCESS
|
||||
import jellypy
|
||||
plex_login = jellypy.CONFIG.HTTP_PLEX_ADMIN or jellypy.CONFIG.ALLOW_GUEST_ACCESS
|
||||
%>
|
||||
<!doctype html>
|
||||
|
||||
@@ -11,28 +11,28 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
|
||||
</head>
|
||||
|
||||
<body style="margin: 0; overflow: auto;">
|
||||
@@ -109,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
|
||||
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
|
||||
<script src="${http_root}js/platform.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script>
|
||||
@@ -159,16 +159,20 @@
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
statusCode: {
|
||||
200: function() {
|
||||
200: function(xhr, status) {
|
||||
window.location = "${redirect_uri or http_root}";
|
||||
},
|
||||
401: function() {
|
||||
401: function(xhr, status) {
|
||||
if (plex) {
|
||||
$('#sign-in-alert').text('Invalid Plex Login.').show();
|
||||
} else {
|
||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||
$('#username').focus();
|
||||
}
|
||||
},
|
||||
429: function(xhr, status) {
|
||||
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
|
||||
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
from plexpy import helpers
|
||||
from jellypy import helpers
|
||||
%>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
@@ -208,7 +208,6 @@
|
||||
<script src="${http_root}js/jquery.dataTables.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/logs.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/plex_logs.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/notification_logs.js${cache_param}"></script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Tautulli - ${title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
% if newsletter:
|
||||
<%!
|
||||
import json
|
||||
from plexpy import notifiers
|
||||
from plexpy.helpers import anon_url, checked
|
||||
from jellypy import notifiers
|
||||
from jellypy.helpers import anon_url, checked
|
||||
|
||||
all_notifiers = sorted(notifiers.get_notifiers(), key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id']))
|
||||
email_notifiers = [n for n in all_notifiers if n['agent_name'] == 'email']
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
<p class="help-block">
|
||||
<span id="simple_cron_message">Set the schedule for the newsletter.</span>
|
||||
<span id="custom_cron_message">Set the schedule for the newsletter using a <a href="${anon_url('https://crontab.guru')}" target="_blank">custom crontab</a>. Only standard cron values are valid.</span>
|
||||
<span id="custom_cron_message">Set the schedule for the newsletter using a <a href="${anon_url('https://crontab.guru')}" target="_blank" rel="noreferrer">custom crontab</a>. Only standard cron values are valid.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Tautulli - ${title} | ${server_name}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
|
||||
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var frame = $('<iframe></iframe>', {
|
||||
|
||||
@@ -9,7 +9,7 @@ Version: 0.1
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<% from plexpy import newsletter_handler %>
|
||||
<% from jellypy import newsletter_handler %>
|
||||
<ul class="stacked-configs list-unstyled">
|
||||
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
|
||||
<li class="newsletter-agent pointer" data-id="${newsletter['id']}">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
% if notifier:
|
||||
<%
|
||||
import json
|
||||
from plexpy import notifiers, users
|
||||
from plexpy.helpers import checked
|
||||
from jellypy import notifiers, users
|
||||
from jellypy.helpers import checked
|
||||
available_notification_actions = notifiers.available_notification_actions(agent_id=notifier['agent_id'])
|
||||
|
||||
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%
|
||||
import datetime
|
||||
import plexpy
|
||||
from plexpy import activity_handler, helpers
|
||||
import jellypy
|
||||
from jellypy import activity_handler, helpers
|
||||
|
||||
if queue == 'active sessions':
|
||||
filter_key = 'session_key-'
|
||||
|
||||
@@ -32,7 +32,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.helpers import cast_to_int, page
|
||||
from jellypy.helpers import cast_to_int, page
|
||||
%>
|
||||
% if data:
|
||||
<div class="dashboard-recent-media-row">
|
||||
|
||||
@@ -11,10 +11,10 @@ DOCUMENTATION :: END
|
||||
|
||||
<%!
|
||||
import datetime
|
||||
import plexpy
|
||||
from plexpy import common, helpers
|
||||
import jellypy
|
||||
from jellypy import common, helpers
|
||||
|
||||
scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
|
||||
scheduled_jobs = [j.id for j in jellypy.SCHED.get_jobs()]
|
||||
%>
|
||||
|
||||
<table class="config-scheduler-table small-muted">
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import plexpy
|
||||
from plexpy import common, notifiers, newsletters
|
||||
from plexpy.helpers import anon_url, checked
|
||||
import jellypy
|
||||
from jellypy import common, notifiers, newsletters
|
||||
from jellypy.helpers import anon_url, checked
|
||||
|
||||
docker_setting = 'disabled' if plexpy.DOCKER else ''
|
||||
docker_msg = '<span class="setting-message small">(Controlled by Docker Container)</span>' if plexpy.DOCKER else ''
|
||||
docker_setting = 'disabled' if jellypy.DOCKER else ''
|
||||
docker_msg = '<span class="setting-message small">(Controlled by Docker Container)</span>' if jellypy.DOCKER else ''
|
||||
|
||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'].lower())
|
||||
available_newsletter_agents = sorted(newsletters.available_newsletter_agents(), key=lambda k: k['label'].lower())
|
||||
@@ -17,8 +17,6 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
|
||||
<link href="${http_root}css/selectize.min.css" rel="stylesheet" />
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
@@ -48,17 +46,17 @@
|
||||
<!-- Nav tabs -->
|
||||
<div class="col-md-3">
|
||||
<ul class="nav-settings list-unstyled" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#tabs-help_info" aria-controls="tabs-help_info" role="tab" data-toggle="tab">Help & Info</a></li>
|
||||
<li role="presentation"><a href="#tabs-general" aria-controls="tabs-general" role="tab" data-toggle="tab">General</a></li>
|
||||
<li role="presentation"><a href="#tabs-homepage" aria-controls="tabs-homepage" role="tab" data-toggle="tab">Homepage</a></li>
|
||||
<li role="presentation"><a href="#tabs-web_interface" aria-controls="tabs-web_interface" role="tab" data-toggle="tab">Web Interface</a></li>
|
||||
<li role="presentation"><a href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
|
||||
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</a></li>
|
||||
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
|
||||
<li role="presentation"><a href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
|
||||
<li role="presentation"><a href="#tabs-3rd_party_apis" aria-controls="tabs-3rd_party_apis" role="tab" data-toggle="tab">3rd Party APIs</a></li>
|
||||
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
|
||||
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
|
||||
<li role="presentation" class="active"><a id="nav-help_info" href="#tabs-help_info" aria-controls="tabs-help_info" role="tab" data-toggle="tab">Help & Info</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-general" href="#tabs-general" aria-controls="tabs-general" role="tab" data-toggle="tab">General</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-homepage" href="#tabs-homepage" aria-controls="tabs-homepage" role="tab" data-toggle="tab">Homepage</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-web_interface" href="#tabs-web_interface" aria-controls="tabs-web_interface" role="tab" data-toggle="tab">Web Interface</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-plex_media_server" href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-notifications" href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-notification_agents" href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-newsletter_agents" href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-3rd_party_apis" href="#tabs-3rd_party_apis" aria-controls="tabs-3rd_party_apis" role="tab" data-toggle="tab">3rd Party APIs</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-import_backups" href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-android_app" href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
@@ -222,7 +220,7 @@
|
||||
<p class="help-block">Check for Tautulli updates periodically.</p>
|
||||
</div>
|
||||
<div id="git_update_options">
|
||||
% if not plexpy.FROZEN:
|
||||
% if not plexpy.SNAP and not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
|
||||
@@ -830,7 +828,7 @@
|
||||
<p class="help-block">
|
||||
Optional: Set your Plex logs folder to use Tautulli as a log viewer. Plex logs are not needed for Tautulli to function.
|
||||
A complete folder path is required, shortcuts are not recognized, and the logs must be accessible from the machine where Tautulli is installed.
|
||||
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank">Click here</a> for help.
|
||||
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank" rel="noreferrer">Click here</a> for help.
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
@@ -979,15 +977,21 @@
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Consecutive Notifications
|
||||
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Playback Stop Notifications Exceeding Watched Percent
|
||||
</label>
|
||||
<p class="help-block">Enable to allow sending of consecutive notifications (i.e. both watched & stopped notifications).</p>
|
||||
<p class="help-block">
|
||||
Enable to allow sending of playback stop notifications after the watched percent is exceeded.
|
||||
Disable to only send playback stop notifications below the watched percent.
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_concurrent_by_ip" id="notify_concurrent_by_ip" value="1" ${config['notify_concurrent_by_ip']}> User Concurrent Streams Notifications by IP Address
|
||||
</label>
|
||||
<p class="help-block">Enable to only send a notification of concurrent streams by a single user from different IP addresses.</p>
|
||||
<p class="help-block">
|
||||
Enable to only send a concurrent streams notification by a single user from different IP addresses.
|
||||
Disable to send a concurrent streams notification anytime the concurrent stream threshold is exceeded regardless of IP address.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notify_concurrent_threshold">User Concurrent Stream Threshold</label>
|
||||
@@ -999,6 +1003,15 @@
|
||||
</div>
|
||||
<p class="help-block">The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_new_device_initial_only" id="notify_new_device_initial_only" value="1" ${config['notify_new_device_initial_only']}> User New Device Notification First Time Only
|
||||
</label>
|
||||
<p class="help-block">
|
||||
Enable to only send a new device notification the first time a user streams from a new device.
|
||||
Disable to send a new device notification everytime a user streams from the device until it is recorded in history (i.e. exceeds the ignore interval).
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label for="notify_concurrent_threshold">Continued Session Threshold</label>
|
||||
<div class="row">
|
||||
@@ -1037,7 +1050,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notify_recently_added_delay">Notification Delay</label>
|
||||
<label for="notify_recently_added_delay">Recently Added Notification Delay</label>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="notify_recently_added_delay" name="notify_recently_added_delay" value="${config['notify_recently_added_delay']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_recently_added_delay_error" required>
|
||||
@@ -1070,6 +1083,21 @@
|
||||
</p>
|
||||
</div>-->
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Remote Access Notifications</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notify_remote_access_threshold">Remote Access Down Threshold</label>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="notify_remote_access_threshold" name="notify_remote_access_threshold" value="${config['notify_remote_access_threshold']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_remote_access_threshold_error" required>
|
||||
</div>
|
||||
<div id="notify_remote_access_threshold_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
<p class="help-block">The duration (in seconds) for Plex remote access to be down before sending a notification. Minimum 60, default 60.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Newsletters</h3>
|
||||
</div>
|
||||
@@ -1164,7 +1192,7 @@
|
||||
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
|
||||
</p>
|
||||
<p class="help-block">
|
||||
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>Notification Agents Guide</a> for instructions on setting up each notification agent.
|
||||
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">Notification Agents Guide</a> for instructions on setting up each notification agent.
|
||||
</p>
|
||||
<br />
|
||||
<div id="plexpy-notifiers-table">
|
||||
@@ -1226,7 +1254,7 @@
|
||||
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
||||
<div class="form-group">
|
||||
<p class="help-block">
|
||||
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up Imgur.<br>
|
||||
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">3rd Party APIs Guide</a> for instructions on setting up Imgur.<br>
|
||||
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1249,7 +1277,7 @@
|
||||
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
||||
<div class="form-group">
|
||||
<p class="help-block">
|
||||
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up Cloudinary.
|
||||
Please see the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">3rd Party APIs Guide</a> for instructions on setting up Cloudinary.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -1442,6 +1470,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="export_dir">Export Directory</label> ${docker_msg | n}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control directory-settings" id="export_dir" name="export_dir" value="${config['export_dir']}" ${docker_setting}>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-form" type="button" id="export_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#export_dir" ${docker_setting}>Browse</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-form" type="button" id="clear_exports">Clear All Exports</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||
|
||||
@@ -1456,9 +1500,9 @@
|
||||
<div class="form-group">
|
||||
<label>Get the App</label>
|
||||
<p class="help-block">
|
||||
Get the <a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank">Tautulli Remote</a> app on Google Play<sup>TM</sup> to access Tautulli from your Android device!<br />
|
||||
Get the <a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank" rel="noreferrer">Tautulli Remote</a> app on Google Play<sup>TM</sup> to access Tautulli from your Android device!<br />
|
||||
<span class="google-play-badge">
|
||||
<a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank"><img alt="Get it on Google Play" src="images/en-play-badge.png" /></a>
|
||||
<a href="${anon_url('https://play.google.com/store/apps/details?id=com.williamcomartin.plexpyremote')}" target="_blank" rel="noreferrer"><img alt="Get it on Google Play" src="images/en-play-badge.png" /></a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1494,14 +1538,14 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
|
||||
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Issues/blob/master/README.md' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">guidelines</a>
|
||||
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Issues/blob/master/README.md' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">guidelines</a>
|
||||
in the README document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
|
||||
<br /><br />
|
||||
Your post may be removed for failure to follow the guidelines.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" target="_blank" id="guidelines-continue" class="btn btn-bright">Continue</a>
|
||||
<a href="#" target="_blank" rel="noreferrer" id="guidelines-continue" class="btn btn-bright">Continue</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1516,12 +1560,12 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
|
||||
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">FAQ</a>
|
||||
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer">FAQ</a>
|
||||
before asking for help!</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" target="_blank" id="support-continue" class="btn btn-bright">Continue</a>
|
||||
<a href="#" target="_blank" rel="noreferrer" id="support-continue" class="btn btn-bright">Continue</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1971,8 +2015,7 @@ Rating: {rating}/10 --> Rating: /10
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="${http_root}js/parsley.min.js"></script>
|
||||
<script src="${http_root}js/Sortable.min.js"></script>
|
||||
<script src="${http_root}js/selectize.min.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/jquery.inputaffix.min.js"></script>
|
||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||
<script>
|
||||
function getConfigurationTable() {
|
||||
@@ -1981,7 +2024,7 @@ Rating: {rating}/10 --> Rating: /10
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#plexpy-configuration-table").html(xhr.responseText);
|
||||
$("#jellypy-configuration-table").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1992,7 +2035,7 @@ Rating: {rating}/10 --> Rating: /10
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#plexpy-scheduler-table").html(xhr.responseText);
|
||||
$("#jellypy-scheduler-table").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2003,7 +2046,7 @@ Rating: {rating}/10 --> Rating: /10
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#plexpy-notifiers-table").html(xhr.responseText);
|
||||
$("#jellypy-notifiers-table").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2028,7 +2071,7 @@ Rating: {rating}/10 --> Rating: /10
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#plexpy-newsletters-table").html(xhr.responseText);
|
||||
$("#jellypy-newsletters-table").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2053,7 +2096,7 @@ Rating: {rating}/10 --> Rating: /10
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#plexpy-mobile-devices-table").html(xhr.responseText);
|
||||
$("#jellypy-mobile-devices-table").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2124,9 +2167,9 @@ $(document).ready(function() {
|
||||
|
||||
// Javascript to enable link to tab
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
var prefix = "tabs_";
|
||||
if (hash) {
|
||||
$('.nav-settings a[href='+hash.replace(prefix,"")+']').tab('show');
|
||||
$('.nav-settings #nav-' + hash.replace('#' + prefix, "")).tab('show');
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
@@ -2151,6 +2194,7 @@ $(document).ready(function() {
|
||||
|
||||
function preSaveChecks(_callback) {
|
||||
verifyPMSWebURL();
|
||||
setBaseURLSuffix(true);
|
||||
if (serverChanged) {
|
||||
verifyServer(_callback);
|
||||
} else if (typeof _callback === "function") {
|
||||
@@ -2170,6 +2214,7 @@ $(document).ready(function() {
|
||||
getNewslettersTable();
|
||||
getMobileDevicesTable();
|
||||
loadUpdateDistros();
|
||||
setBaseURLSuffix();
|
||||
settingsChanged = false;
|
||||
}
|
||||
|
||||
@@ -2292,6 +2337,12 @@ $(document).ready(function() {
|
||||
confirmAjaxCall(url, msg);
|
||||
});
|
||||
|
||||
$("#clear_exports").click(function () {
|
||||
var msg = 'Are you sure you want to clear the Tautulli metadata exports?';
|
||||
var url = 'delete_export?delete_all=true';
|
||||
confirmAjaxCall(url, msg);
|
||||
});
|
||||
|
||||
$("#clear_logs").click(function () {
|
||||
var msg = 'Are you sure you want to clear the Tautulli logs?';
|
||||
var url = 'delete_logs';
|
||||
@@ -2875,10 +2926,19 @@ $(document).ready(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$('#http_base_url').change(function () {
|
||||
$(this).val($(this).val().replace(/\/*$/, ''));
|
||||
$('#http_root').change(function() {
|
||||
setBaseURLSuffix();
|
||||
});
|
||||
|
||||
function setBaseURLSuffix(clear) {
|
||||
if (clear){
|
||||
$('#http_base_url').suffix("");
|
||||
} else {
|
||||
$('#http_base_url').suffix($('#http_root').val());
|
||||
}
|
||||
}
|
||||
setBaseURLSuffix();
|
||||
|
||||
function apiEnabled() {
|
||||
var api_enabled = $('#api_enabled').prop('checked');
|
||||
$('#app_api_msg').toggle(!(api_enabled));
|
||||
@@ -2997,7 +3057,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
function gotoSetting(tab, setting){
|
||||
$("a[href=#tabs-" + tab + "]").click();
|
||||
$("#nav-tabs-" + tab).click();
|
||||
if (setting) {
|
||||
_setting = '#' + setting;
|
||||
if ($(_setting).closest('.advanced-setting').length && !$('#menu_link_show_advanced_settings').hasClass('active')) {
|
||||
@@ -3045,7 +3105,7 @@ $(document).ready(function() {
|
||||
if (news_item.subtitle) { content.append(subtitle); }
|
||||
content.append(body);
|
||||
var li = $('<li/>').append(header).append(content)
|
||||
if (index === 0 && Math.abs(now.diff(date, 'days')) < 7) {
|
||||
if (index === 0 && Math.abs(now.diff(date, 'days')) <= 30) {
|
||||
li.addClass('open');
|
||||
content.css('display', 'block');
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||
from jellypy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||
%>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
from plexpy.helpers import anon_url
|
||||
from jellypy.helpers import anon_url
|
||||
%>
|
||||
<%def name="headIncludes()">
|
||||
</%def>
|
||||
@@ -16,10 +16,10 @@
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-dark" href="${anon_url('https://tautulli.com/discord')}" target="_blank"><i class="fab fa-discord"></i> Join Discord</a>
|
||||
<a class="btn btn-dark" href="${anon_url('https://tautulli.com/discord')}" target="_blank" rel="noreferrer"><i class="fab fa-discord"></i> Join Discord</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-dark" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank"><i class="fab fa-reddit"></i> Join Reddit</a>
|
||||
<a class="btn btn-dark" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank" rel="noreferrer"><i class="fab fa-reddit"></i> Join Reddit</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark" id="popout-iframe-button"><i class="fa fa-external-link"></i> Pop Out Chat</button>
|
||||
|
||||
@@ -171,6 +171,7 @@ DOCUMENTATION :: END
|
||||
</p>
|
||||
<p> with </p>
|
||||
<p><span id="new_title"></span></p>
|
||||
<p>from the <strong><span id="new_library"></span></strong> library?</p>
|
||||
% if query['media_type'] != 'movie':
|
||||
<p>All items for <strong>${query['grandparent_title']}</strong> will also be updated.</p>
|
||||
% endif
|
||||
@@ -211,10 +212,12 @@ DOCUMENTATION :: END
|
||||
|
||||
$(document).on('click', '#search-results-list a', function (e) {
|
||||
e.preventDefault();
|
||||
var new_rating_key = $(this).attr('id');
|
||||
var new_rating_key = $(this).data('rating_key');
|
||||
var new_library_section = $(this).data('library_name');
|
||||
var new_href = $(this).attr('href');
|
||||
|
||||
$('#new_title').html($(this).find('.item-children-instance-text-wrapper').html());
|
||||
$('#new_library').text(new_library_section);
|
||||
|
||||
$('#confirm-modal-update').modal();
|
||||
$('#confirm-modal-update').one('click', '#confirm-update', function () {
|
||||
|
||||
@@ -26,7 +26,7 @@ DOCUMENTATION :: END
|
||||
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
from plexpy import helpers
|
||||
from jellypy import helpers
|
||||
%>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
@@ -43,7 +43,9 @@ DOCUMENTATION :: END
|
||||
<div class="summary-navbar">
|
||||
<div class="col-md-12">
|
||||
<div class="summary-navbar-list">
|
||||
<ul class="list-unstyled breadcrumb"></ul>
|
||||
<ul class="list-unstyled breadcrumb">
|
||||
<li class="active">${data['friendly_name']}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,12 +69,16 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="user-info-nav">
|
||||
<ul class="user-info-nav" role="tablist">
|
||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
<li><a id="sync-tab-btn" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
|
||||
<li><a id="ip-tab-btn" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
|
||||
<li><a id="login-tab-btn" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
|
||||
<ul class="nav nav-list nav-pills" role="tablist">
|
||||
<li class="active"><a id="nav-tabs-profile" href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
<li><a id="nav-tabs-playlists" href="#tabs-playlists" role="tab" data-toggle="tab">Playlists</a></li>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<li><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
|
||||
% endif
|
||||
<li><a id="nav-tabs-synceditems" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
|
||||
<li><a id="nav-tabs-ipaddresses" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
|
||||
<li><a id="nav-tabs-tautullilogins" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,6 +216,99 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-playlists">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-list-alt"></i> Playlists for <strong>
|
||||
<span class="set-username">${data['friendly_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark export-button" id="toggle-export-modal" data-toggle="modal" data-target="#export-modal"
|
||||
data-user_id="${data['user_id']}" data-media_type="playlist" data-sub_media_type="video,audio,photo"
|
||||
data-export_type="playlist">
|
||||
<i class="fa fa-file-export"></i> Export playlists
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-playlists-table-button" id="refresh-playlists-table">
|
||||
<i class="fa fa-refresh"></i> Refresh playlists
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-playlists"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display playlists_table" id="playlists_table-SID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="playlistTitle">Playlist Title</th>
|
||||
<th align="left" id="playlistLeafCount">Playlist Items</th>
|
||||
<th align="left" id="playlistDuration">Playlist Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-export">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-file-export"></i> Metadata Exports for <strong>
|
||||
<span class="set-username">${data['friendly_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
|
||||
<i class="fa fa-refresh"></i> Refresh exports
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display export_table" id="export_table-SID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="timestamp">Exported At</th>
|
||||
<th align="left" id="media_type_title">Media Type</th>
|
||||
<th align="left" id="rating_key">Rating Key</th>
|
||||
<th align="left" id="filename">Filename</th>
|
||||
<th align="left" id="file_format">File Format</th>
|
||||
<th align="left" id="metadata_level">Metadata Level</th>
|
||||
<th align="left" id="media_info_level">Media Info Level</th>
|
||||
<th align="left" id="media_info_level">Custom Fields</th>
|
||||
<th align="left" id="file_size">File Size</th>
|
||||
<th align="left" id="complete">Download</th>
|
||||
<th align="left" id="delete">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-synceditems">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
@@ -278,6 +377,7 @@ DOCUMENTATION :: END
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-ip-address-button" id="refresh-ip-address-list"><i class="fa fa-refresh"></i> Refresh IP addresses</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-ip-address"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
@@ -393,6 +493,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
@@ -410,8 +512,9 @@ DOCUMENTATION :: END
|
||||
|
||||
var username = '${data['username'].replace("'", "\\'")}';
|
||||
</script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/playlists_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/user_ips.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/login_logs.js${cache_param}"></script>
|
||||
@@ -420,6 +523,8 @@ DOCUMENTATION :: END
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
$(".inactive-user-tooltip").tooltip();
|
||||
|
||||
function loadHistoryTable(media_type) {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
@@ -451,6 +556,49 @@ DOCUMENTATION :: END
|
||||
});
|
||||
}
|
||||
|
||||
$('#nav-tabs-history').on('shown.bs.tab', function() {
|
||||
if (typeof(history_table) === 'undefined') {
|
||||
var media_type = getLocalStorage('user_' + user_id + '-history_media_type', 'all');
|
||||
$('#history-' + media_type).prop('checked', true);
|
||||
$('#history-' + media_type).closest('label').addClass('active');
|
||||
loadHistoryTable(media_type);
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
function loadPlaylistsTable() {
|
||||
// Build playlists table
|
||||
playlists_table_options.ajax = {
|
||||
url: 'get_playlists_list',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
user_id: user_id
|
||||
};
|
||||
}
|
||||
};
|
||||
playlists_table = $('#playlists_table-SID-${data["user_id"]}').DataTable(playlists_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(playlists_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-playlists');
|
||||
|
||||
clearSearchButton('playlists_table-SID-${data["user_id"]}', playlists_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-playlists').on('shown.bs.tab', function() {
|
||||
if (typeof(playlists_table) === 'undefined') {
|
||||
loadPlaylistsTable();
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-playlists-table").click(function () {
|
||||
playlists_table.draw();
|
||||
});
|
||||
|
||||
function loadSyncTable() {
|
||||
// Build user sync table
|
||||
sync_table_options.ajax = {
|
||||
@@ -466,6 +614,16 @@ DOCUMENTATION :: END
|
||||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-synceditems').on('shown.bs.tab', function() {
|
||||
if (typeof(sync_table) === 'undefined') {
|
||||
loadSyncTable(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-syncs-list").click(function() {
|
||||
sync_table.ajax.reload();
|
||||
});
|
||||
|
||||
function loadIPAddressTable() {
|
||||
// Build user IP table
|
||||
user_ip_table_options.ajax = {
|
||||
@@ -480,9 +638,22 @@ DOCUMENTATION :: END
|
||||
};
|
||||
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
|
||||
|
||||
var colvis_user_ip = new $.fn.dataTable.ColVis( user_ip_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark'} );
|
||||
$( colvis_user_ip.button() ).appendTo('#button-bar-ip-address');
|
||||
|
||||
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-ipaddresses').on('shown.bs.tab', function() {
|
||||
if (typeof(user_ip_table) === 'undefined') {
|
||||
loadIPAddressTable(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-ip-address-list").click(function () {
|
||||
user_ip_table.draw();
|
||||
});
|
||||
|
||||
function loadLoginTable() {
|
||||
// Build user login table
|
||||
login_log_table_options.ajax = {
|
||||
@@ -504,52 +675,141 @@ DOCUMENTATION :: END
|
||||
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
||||
}
|
||||
|
||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
||||
if (typeof(history_table) === 'undefined') {
|
||||
var media_type = getLocalStorage('user_' + user_id + '-history_media_type', 'all');
|
||||
$('#history-' + media_type).prop('checked', true);
|
||||
$('#history-' + media_type).closest('label').addClass('active');
|
||||
loadHistoryTable(media_type);
|
||||
}
|
||||
});
|
||||
|
||||
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
|
||||
if (typeof(sync_table) === 'undefined') {
|
||||
loadSyncTable(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
|
||||
if (typeof(user_ip_table) === 'undefined') {
|
||||
loadIPAddressTable(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
|
||||
$('#nav-tabs-tautullilogins').on('shown.bs.tab', function() {
|
||||
if (typeof(login_log_table) === 'undefined') {
|
||||
loadLoginTable(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
$("#refresh-syncs-list").click(function() {
|
||||
sync_table.ajax.reload();
|
||||
});
|
||||
|
||||
$("#refresh-ip-address-list").click(function () {
|
||||
user_ip_table.draw();
|
||||
});
|
||||
|
||||
$("#refresh-login-list").click(function () {
|
||||
login_log_table.draw();
|
||||
});
|
||||
|
||||
$(".inactive-user-tooltip").tooltip();
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
user_id: user_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#user-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
highlightWatchedScrollerButton();
|
||||
});
|
||||
|
||||
var leftTotal = 0;
|
||||
$(".paginate").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#user-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
// Javascript to enable link to tab
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
if (hash) {
|
||||
$('.nav-list #nav-' + hash.replace('#' + prefix, "")).tab('show').trigger('show.bs.tab');
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
$('.nav-list a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||
});
|
||||
|
||||
// Populate watch time stats
|
||||
$.ajax({
|
||||
url: 'user_watch_time_stats',
|
||||
async: true,
|
||||
data: { user_id: user_id, user: username },
|
||||
complete: function(xhr, status) {
|
||||
$("#user-time-stats").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
// Populate platform stats
|
||||
$.ajax({
|
||||
url: 'user_player_stats',
|
||||
async: true,
|
||||
data: { user_id: user_id, user: username },
|
||||
complete: function(xhr, status) {
|
||||
$("#user-player-stats").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<script>
|
||||
function loadExportTable() {
|
||||
// Build export table
|
||||
export_table_options.ajax = {
|
||||
url: 'get_export_list',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
user_id: user_id
|
||||
};
|
||||
}
|
||||
};
|
||||
export_table = $('#export_table-SID-${data["user_id"]}').DataTable(export_table_options);
|
||||
export_table.columns([2, 7]).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-export');
|
||||
|
||||
clearSearchButton('export_table-SID-${data["user_id"]}', export_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-export').on('shown.bs.tab', function() {
|
||||
if (typeof(export_table) === 'undefined') {
|
||||
loadExportTable();
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-export-table").click(function () {
|
||||
export_table.draw();
|
||||
});
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
$("#edit-user-tooltip").tooltip();
|
||||
|
||||
// Load edit user modal
|
||||
@@ -566,6 +826,23 @@ DOCUMENTATION :: END
|
||||
});
|
||||
});
|
||||
|
||||
$(".export-button").click(function() {
|
||||
$.ajax({
|
||||
url: 'export_metadata_modal',
|
||||
data: {
|
||||
user_id: $(this).data('user_id'),
|
||||
media_type: $(this).data('media_type'),
|
||||
sub_media_type: $(this).data('sub_media_type'),
|
||||
export_type: $(this).data('export_type')
|
||||
},
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#export-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
@@ -644,100 +921,7 @@ DOCUMENTATION :: END
|
||||
});
|
||||
}
|
||||
});
|
||||
% endif
|
||||
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
user_id: user_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#user-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
highlightWatchedScrollerButton();
|
||||
});
|
||||
|
||||
var leftTotal = 0;
|
||||
$(".paginate").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#user-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
// Javascript to enable link to tab
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
if (hash) {
|
||||
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
$('.user-info-nav a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||
});
|
||||
|
||||
// Populate watch time stats
|
||||
$.ajax({
|
||||
url: 'user_watch_time_stats',
|
||||
async: true,
|
||||
data: { user_id: user_id, user: username },
|
||||
complete: function(xhr, status) {
|
||||
$("#user-time-stats").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
// Populate platform stats
|
||||
$.ajax({
|
||||
url: 'user_player_stats',
|
||||
async: true,
|
||||
data: { user_id: user_id, user: username },
|
||||
complete: function(xhr, status) {
|
||||
$("#user-player-stats").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% endif
|
||||
</%def>
|
||||
@@ -28,7 +28,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<%
|
||||
from plexpy.helpers import page
|
||||
from jellypy.helpers import page
|
||||
%>
|
||||
<div class="dashboard-recent-media-row">
|
||||
<div id="recently-watched-row-scroller" style="left: 0;">
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/users.js${cache_param}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy import common, helpers
|
||||
import jellypy
|
||||
from jellypy import common, helpers
|
||||
%>
|
||||
|
||||
<!doctype html>
|
||||
@@ -12,7 +12,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/bootstrap-wizard.css" rel="stylesheet">
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet">
|
||||
@@ -21,21 +21,21 @@
|
||||
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -50,10 +50,7 @@
|
||||
<h3 style="line-height: 50px;">Welcome!</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="welcome-message">
|
||||
Thanks for taking the time to try out Tautulli. Hope you find it useful.
|
||||
</p>
|
||||
<p class="welcome-message">
|
||||
Tautulli requires a permanent internet connection to ensure a reliable experience.
|
||||
Thanks for taking the time to try out JellyPy. Hope you find it useful.
|
||||
</p>
|
||||
<p class="welcome-message">
|
||||
This wizard will help you get set up, to continue press Next.
|
||||
@@ -65,52 +62,43 @@
|
||||
<h3>Authentication</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
Please setup an admin username and password for Tautulli.
|
||||
Please setup an admin username and password for JellyPy.
|
||||
</p>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="http_username">HTTP Username</label>
|
||||
<label for="http_username">Username</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<input type="text" class="form-control auth-settings" id="http_username" name="http_username" value="" size="30">
|
||||
<input type="text" class="form-control auth-settings" id="http_username"
|
||||
name="http_username" value="" size="30">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="http_password">HTTP Password</label>
|
||||
<label for="http_password">Password</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<input type="password" class="form-control auth-settings" id="http_password" name="http_password" value="" size="30" autocomplete="new-password">
|
||||
<input type="password" class="form-control auth-settings" id="http_password"
|
||||
name="http_password" value="" size="30" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" class="form-control" name="http_hash_password" id="http_hash_password" value="1">
|
||||
<input type="hidden" class="form-control" name="http_hash_password" id="http_hash_password"
|
||||
value="1">
|
||||
<input type="hidden" class="form-control" name="http_plex_admin" id="http_plex_admin" value="1">
|
||||
<input type="hidden" id="authentication_valid" data-validate="validateAuthentication" value="">
|
||||
<span style="display: none;" id="authentication-status"></span>
|
||||
</div>
|
||||
|
||||
<div class="wizard-card" data-cardname="card3">
|
||||
<h3>Plex Account</h3>
|
||||
<h3>Jellyfin Server</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
Tautulli requires a Plex.tv account. Click the button below to sign in on Plex.tv. You may need to allow popups in your browser.
|
||||
</p>
|
||||
</div>
|
||||
<input type="hidden" class="form-control" name="pms_token" id="pms_token" value="" data-validate="validatePMStoken">
|
||||
<a class="btn btn-dark" id="sign-in-plex" href="#" role="button">Sign In with Plex</a>
|
||||
<span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
||||
</div>
|
||||
|
||||
<div class="wizard-card" data-cardname="card4">
|
||||
<h3>Plex Media Server</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.
|
||||
Select your Jellyfin Server from the dropdown menu or enter an IP address or hostname.
|
||||
</p>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="pms_ip_selectize">Plex IP Address or Hostname</label>
|
||||
<label for="pms_ip_selectize">Jellyfin IP Address or Hostname</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip_selectize">
|
||||
@@ -123,22 +111,26 @@
|
||||
data-ssl="${config['pms_ssl']}"
|
||||
data-is_cloud="${config['pms_is_cloud']}"
|
||||
data-label="${config['pms_name'] or 'Local'}"
|
||||
selected>${config['pms_ip']}</option>
|
||||
selected>${config['pms_ip']}
|
||||
</option>
|
||||
% endif
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="pms_port">Plex Port</label>
|
||||
<label for="pms_port">Jellyfin Port</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
||||
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port"
|
||||
placeholder="8096" value="${config['pms_port']}" required>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
|
||||
<input type="checkbox" id="pms_ssl_checkbox"
|
||||
class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1"
|
||||
${helpers.checked(config['pms_ssl'])}> Use SSL
|
||||
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
|
||||
</label>
|
||||
</div>
|
||||
@@ -146,16 +138,44 @@
|
||||
<div class="col-xs-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
|
||||
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
|
||||
<input type="checkbox" id="pms_is_remote_checkbox"
|
||||
class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1"
|
||||
${helpers.checked(config['pms_is_remote'])}> Remote Server
|
||||
<input type="hidden" id="pms_is_remote" name="pms_is_remote"
|
||||
value="${config['pms_is_remote']}">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="pms_valid" data-validate="validatePMSip" value="">
|
||||
<input type="hidden" id="pms_ip" name="pms_ip" value="${config['pms_ip']}">
|
||||
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
|
||||
</div>
|
||||
|
||||
<div class="wizard-card" data-cardname="card4">
|
||||
<h3>Jellyfin Auth</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
Authentificate with your Jellyfin instance.
|
||||
</p>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="jellyfin_user">Jellyfin Username</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<input type="text" class="form-control pms-settings" name="jellyfin_user"
|
||||
id="jellyfin_user" value="" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="jellyfin_password">Jellyfin Password</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<input type="password" class="form-control pms-settings" name="jellyfin_password"
|
||||
id="jellyfin_password" value="" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a>
|
||||
<span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
||||
@@ -172,15 +192,21 @@
|
||||
<label for="logging_ignore_interval">Ignore Interval</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<input type="text" class="form-control pms-monitoring" id="logging_ignore_interval" name="logging_ignore_interval" placeholder="120" value="${config['logging_ignore_interval']}" data-validate="validateIgnoreInterval" required>
|
||||
<input type="text" class="form-control pms-monitoring" id="logging_ignore_interval"
|
||||
name="logging_ignore_interval" placeholder="120"
|
||||
value="${config['logging_ignore_interval']}"
|
||||
data-validate="validateIgnoreInterval" required>
|
||||
</div>
|
||||
<span style="margin-left: 10px; line-height: 35px; display: none;" id="ignore-int-status"></span>
|
||||
<span style="margin-left: 10px; line-height: 35px; display: none;"
|
||||
id="ignore-int-status"></span>
|
||||
</div>
|
||||
<p class="help-block">The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.</p>
|
||||
<p class="help-block">The interval (in seconds) an item must be in a playing state before
|
||||
logging it. 0 to disable.</p>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
Additional options to disable history logging for certain libraries or users can be found by editing them
|
||||
Additional options to disable history logging for certain libraries or users can be found by
|
||||
editing them
|
||||
on the <strong>Libraries</strong> or <strong>Users</strong> pages.
|
||||
</p>
|
||||
</div>
|
||||
@@ -190,11 +216,13 @@
|
||||
<h3>Notifications</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
Tautulli can send a wide variety of notifications to alert you of activity on your Plex server.
|
||||
Tautulli can send a wide variety of notifications to alert you of activity on your Plex
|
||||
server.
|
||||
</p>
|
||||
<p class="help-block">
|
||||
To set up a notification agent, navigate to the <strong>Settings</strong> page
|
||||
and to the <strong>Notification Agents</strong> tab after you have completed this setup wizard.
|
||||
and to the <strong>Notification Agents</strong> tab after you have completed this setup
|
||||
wizard.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +231,8 @@
|
||||
<h3>Database Import</h3>
|
||||
<div class="wizard-input-section">
|
||||
<p class="help-block">
|
||||
If you have an existing Tautulli, PlexWatch, or Plexivity database, you can import the data into Tautulli.
|
||||
If you have an existing Tautulli, PlexWatch, or Plexivity database, you can import the data
|
||||
into Tautulli.
|
||||
</p>
|
||||
<p class="help-block">
|
||||
To import a database, navigate to the <strong>Settings</strong> page
|
||||
@@ -215,18 +244,23 @@
|
||||
<div style="display: none;">
|
||||
<input type="checkbox" name="first_run" id="first_run" value="1" checked>
|
||||
<input type="checkbox" name="group_history_tables" id="group_history_tables" value="1" checked>
|
||||
<input type="checkbox" name="history_table_activity" id="history_table_activity" value="1" checked>
|
||||
<input type="checkbox" name="history_table_activity" id="history_table_activity" value="1"
|
||||
checked>
|
||||
<input type="checkbox" name="sys_tray_icon" id="sys_tray_icon" value="1" checked>
|
||||
<input type="checkbox" name="launch_startup" id="launch_startup" value="1" checked>
|
||||
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" checked>
|
||||
<input type="checkbox" name="api_enabled" id="api_enabled" value="1" checked>
|
||||
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" checked>
|
||||
<input type="checkbox" name="refresh_libraries_on_startup" id="refresh_libraries_on_startup" value="1" checked>
|
||||
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1"
|
||||
checked>
|
||||
<input type="checkbox" name="refresh_libraries_on_startup" id="refresh_libraries_on_startup"
|
||||
value="1" checked>
|
||||
<input type="checkbox" name="check_github" id="check_github" value="1" checked>
|
||||
<input type="checkbox" name="log_blacklist" id="log_blacklist" value="1" checked>
|
||||
<input type="checkbox" name="cache_images" id="cache_images" value="1" checked>
|
||||
<input type="checkbox" name="notify_group_recently_added_grandparent" id="notify_group_recently_added_grandparent" value="1" checked>
|
||||
<input type="checkbox" name="notify_group_recently_added_parent" id="notify_group_recently_added_parent" value="1" checked>
|
||||
<input type="checkbox" name="notify_group_recently_added_grandparent"
|
||||
id="notify_group_recently_added_grandparent" value="1" checked>
|
||||
<input type="checkbox" name="notify_group_recently_added_parent"
|
||||
id="notify_group_recently_added_parent" value="1" checked>
|
||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" checked>
|
||||
<input type="checkbox" name="first_run_complete" id="first_run_complete" value="1" checked>
|
||||
<input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
|
||||
@@ -239,16 +273,18 @@
|
||||
<div class="wizard-success">
|
||||
<h3>Setup Complete!</h3>
|
||||
<br/>
|
||||
<p>Setup is now complete. For more configuration options please visit the Settings menu on the home page.</p>
|
||||
<p>Setup is now complete. For more configuration options please visit the Settings menu on the home
|
||||
page.</p>
|
||||
<br/>
|
||||
<i class="fa fa-refresh fa-spin"></i> Waiting <span class="countdown">5</span> seconds to ensure authentication token is registered...
|
||||
<i class="fa fa-refresh fa-spin"></i> Waiting <span class="countdown">5</span> seconds to ensure
|
||||
authentication token is registered...
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
|
||||
<script src="${http_root}js/jquery-3.5.1.min.js"></script>
|
||||
<script src="${http_root}js/bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/selectize.min.js"></script>
|
||||
<script src="${http_root}js/platform.min.js"></script>
|
||||
@@ -258,7 +294,6 @@
|
||||
function validateAuthentication(el) {
|
||||
var http_username = $("#http_username").val();
|
||||
var http_password = $("#http_password").val();
|
||||
var valid_authentication = el.val();
|
||||
var retValue = {};
|
||||
|
||||
if (http_username === "" || http_password === "") {
|
||||
@@ -325,71 +360,73 @@
|
||||
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$(document).ready(function () {
|
||||
|
||||
$.fn.wizard.logging = false;
|
||||
var options = {
|
||||
keyboard : false,
|
||||
contentHeight : 450,
|
||||
contentWidth : 700,
|
||||
backdrop: 'static',
|
||||
buttons: {submitText: 'Finish'},
|
||||
submitUrl: "configUpdate"
|
||||
};
|
||||
var wizard = $("#setup-wizard").wizard(options);
|
||||
wizard.show();
|
||||
$.fn.wizard.logging = false;
|
||||
var options = {
|
||||
keyboard: false,
|
||||
contentHeight: 450,
|
||||
contentWidth: 700,
|
||||
backdrop: 'static',
|
||||
buttons: {submitText: 'Finish'},
|
||||
submitUrl: "configUpdate"
|
||||
};
|
||||
var wizard = $("#setup-wizard").wizard(options);
|
||||
wizard.show();
|
||||
|
||||
// Change button classes
|
||||
wizard.find('.wizard-back').addClass('btn-dark');
|
||||
wizard.on('incrementCard', function(wizard) {
|
||||
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
|
||||
});
|
||||
wizard.on('decrementCard', function(wizard) {
|
||||
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
|
||||
});
|
||||
// Change button classes
|
||||
wizard.find('.wizard-back').addClass('btn-dark');
|
||||
wizard.on('incrementCard', function (wizard) {
|
||||
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
|
||||
});
|
||||
wizard.on('decrementCard', function (wizard) {
|
||||
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
|
||||
});
|
||||
|
||||
wizard.on("submit", function(wizard) {
|
||||
// Probably should not success before we know, but hopefully validation is good enough.
|
||||
wizard.submitSuccess();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: wizard.args.submitUrl,
|
||||
data: wizard.serialize(),
|
||||
dataType: "json",
|
||||
complete: function (data) {
|
||||
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('.checkbox-toggle').click(function () {
|
||||
var configToggle = $(this).data('id');
|
||||
if ($(this).is(':checked')) {
|
||||
$('#'+configToggle).val(1);
|
||||
} else {
|
||||
$('#'+configToggle).val(0);
|
||||
}
|
||||
});
|
||||
|
||||
var $select_pms = $('#pms_ip_selectize').selectize({
|
||||
createOnBlur: true,
|
||||
openOnFocus: true,
|
||||
maxItems: 1,
|
||||
closeAfterSelect: true,
|
||||
sortField: 'label',
|
||||
searchField: ['label', 'value'],
|
||||
inputClass: 'form-control selectize-input',
|
||||
render: {
|
||||
item: function (item, escape) {
|
||||
if (!item.label) {
|
||||
$.extend(item,
|
||||
$(this.revertSettings.$children)
|
||||
.filter('[value="' + item.value + '"]').data()
|
||||
);
|
||||
wizard.on("submit", function (wizard) {
|
||||
// Probably should not success before we know, but hopefully validation is good enough.
|
||||
wizard.submitSuccess();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: wizard.args.submitUrl,
|
||||
data: wizard.serialize(),
|
||||
dataType: "json",
|
||||
complete: function (data) {
|
||||
$(".countdown").countdown(function () {
|
||||
location.reload();
|
||||
}, 5, "");
|
||||
}
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.ip : null;
|
||||
return '<div data-identifier="' + item.clientIdentifier +
|
||||
})
|
||||
});
|
||||
|
||||
$('.checkbox-toggle').click(function () {
|
||||
var configToggle = $(this).data('id');
|
||||
if ($(this).is(':checked')) {
|
||||
$('#' + configToggle).val(1);
|
||||
} else {
|
||||
$('#' + configToggle).val(0);
|
||||
}
|
||||
});
|
||||
|
||||
var $select_pms = $('#pms_ip_selectize').selectize({
|
||||
createOnBlur: true,
|
||||
openOnFocus: true,
|
||||
maxItems: 1,
|
||||
closeAfterSelect: true,
|
||||
sortField: 'label',
|
||||
searchField: ['label', 'value'],
|
||||
inputClass: 'form-control selectize-input',
|
||||
render: {
|
||||
item: function (item, escape) {
|
||||
if (!item.label) {
|
||||
$.extend(item,
|
||||
$(this.revertSettings.$children)
|
||||
.filter('[value="' + item.value + '"]').data()
|
||||
);
|
||||
}
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.ip : null;
|
||||
return '<div data-identifier="' + item.clientIdentifier +
|
||||
'" data-ip="' + item.ip +
|
||||
'" data-port="' + item.port +
|
||||
'" data-local="' + item.local +
|
||||
@@ -399,11 +436,11 @@ $(document).ready(function() {
|
||||
'<span class="item-text">' + escape(label) + '</span>' +
|
||||
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
},
|
||||
option: function (item, escape) {
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.value : null;
|
||||
return '<div data-identifier="' + item.clientIdentifier +
|
||||
},
|
||||
option: function (item, escape) {
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.value : null;
|
||||
return '<div data-identifier="' + item.clientIdentifier +
|
||||
'" data-ip="' + item.ip +
|
||||
'" data-port="' + item.port +
|
||||
'" data-local="' + item.local +
|
||||
@@ -413,152 +450,128 @@ $(document).ready(function() {
|
||||
escape(label) +
|
||||
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
create: function(input) {
|
||||
return {label: '', value: input};
|
||||
},
|
||||
onInitialize: function () {
|
||||
var s = this;
|
||||
this.revertSettings.$children.each(function () {
|
||||
$.extend(s.options[this.value], $(this).data());
|
||||
});
|
||||
},
|
||||
onChange: function (item) {
|
||||
var pms_ip_selected = this.getItem(item)[0];
|
||||
var identifier = $(pms_ip_selected).data('identifier');
|
||||
var ip = $(pms_ip_selected).data('ip');
|
||||
var port = $(pms_ip_selected).data('port');
|
||||
var local = $(pms_ip_selected).data('local');
|
||||
var ssl = $(pms_ip_selected).data('ssl');
|
||||
var is_cloud = $(pms_ip_selected).data('is_cloud');
|
||||
var value = $(pms_ip_selected).data('value');
|
||||
}
|
||||
},
|
||||
create: function (input) {
|
||||
return {label: '', value: input};
|
||||
},
|
||||
onInitialize: function () {
|
||||
var s = this;
|
||||
this.revertSettings.$children.each(function () {
|
||||
$.extend(s.options[this.value], $(this).data());
|
||||
});
|
||||
},
|
||||
onChange: function (item) {
|
||||
var pms_ip_selected = this.getItem(item)[0];
|
||||
var identifier = $(pms_ip_selected).data('identifier');
|
||||
var ip = $(pms_ip_selected).data('ip');
|
||||
var port = $(pms_ip_selected).data('port');
|
||||
var local = $(pms_ip_selected).data('local');
|
||||
var ssl = $(pms_ip_selected).data('ssl');
|
||||
var value = $(pms_ip_selected).data('value');
|
||||
|
||||
$("#pms_valid").val(identifier !== 'undefined' ? 'valid' : '');
|
||||
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i> Server found!' : '').fadeIn('fast');
|
||||
$("#pms_valid").val(identifier !== 'undefined' ? 'valid' : '');
|
||||
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i> Server found!' : '').fadeIn('fast');
|
||||
|
||||
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||
$('#pms_ip').val(ip !== 'undefined' ? ip : value);
|
||||
$('#pms_port').val(port !== 'undefined' ? port : 8096);
|
||||
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||
|
||||
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||
$('#pms_ip').val(ip !== 'undefined' ? ip : value);
|
||||
$('#pms_port').val(port !== 'undefined' ? port : 32400);
|
||||
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
|
||||
|
||||
if (is_cloud === true) {
|
||||
$('#pms_port').prop('readonly', true);
|
||||
$('#pms_is_remote_checkbox').prop('disabled', true);
|
||||
$('#pms_ssl_checkbox').prop('disabled', true);
|
||||
} else {
|
||||
$('#pms_port').prop('readonly', false);
|
||||
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||
}
|
||||
},
|
||||
onDropdownOpen: function() {
|
||||
this.clear();
|
||||
}
|
||||
});
|
||||
var select_pms = $select_pms[0].selectize;
|
||||
|
||||
function getServerOptions(token) {
|
||||
/* Set token and returns server options */
|
||||
$.ajax({
|
||||
url: 'discover',
|
||||
data: {
|
||||
token: token
|
||||
},
|
||||
success: function (result) {
|
||||
if (result) {
|
||||
var existing_ip = $('#pms_ip').val();
|
||||
var existing_port = $('#pms_port').val();
|
||||
result.forEach(function (item) {
|
||||
if (item.ip === existing_ip && item.port === existing_port) {
|
||||
select_pms.updateOption(item.value, item);
|
||||
} else {
|
||||
select_pms.addOption(item);
|
||||
onDropdownOpen: function () {
|
||||
this.clear();
|
||||
}
|
||||
});
|
||||
var select_pms = $select_pms[0].selectize;
|
||||
|
||||
function getServerOptions(token) {
|
||||
/* Set token and returns server options */
|
||||
$.ajax({
|
||||
url: 'discover',
|
||||
data: {
|
||||
token: token
|
||||
},
|
||||
success: function (result) {
|
||||
if (result) {
|
||||
var existing_ip = $('#pms_ip').val();
|
||||
var existing_port = $('#pms_port').val();
|
||||
result.forEach(function (item) {
|
||||
if (item.ip === existing_ip && item.port === existing_port) {
|
||||
select_pms.updateOption(item.value, item);
|
||||
} else {
|
||||
select_pms.addOption(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var pms_verified = false;
|
||||
var authenticated = false;
|
||||
|
||||
$("#verify-plex-server").click(function () {
|
||||
if (!(pms_verified)) {
|
||||
var pms_ip = $("#pms_ip").val().trim();
|
||||
var pms_port = $("#pms_port").val().trim();
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").val();
|
||||
var pms_is_remote = $("#pms_is_remote").val();
|
||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Verifying server...');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
$.ajax({
|
||||
url: 'get_server_id',
|
||||
data: {
|
||||
hostname: pms_ip,
|
||||
port: pms_port,
|
||||
identifier: pms_identifier,
|
||||
ssl: pms_ssl,
|
||||
remote: pms_is_remote
|
||||
},
|
||||
cache: true,
|
||||
async: true,
|
||||
timeout: 5000,
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Error verifying server: ' + textStatus);
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
},
|
||||
success: function (xhr, status) {
|
||||
var result = xhr;
|
||||
var identifier = result.identifier;
|
||||
if (identifier) {
|
||||
$("#pms_identifier").val(identifier);
|
||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
pms_verified = true;
|
||||
$("#pms_valid").val("valid");
|
||||
} else {
|
||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please enter both fields.');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
var pms_verified = false;
|
||||
var authenticated = false;
|
||||
});
|
||||
|
||||
$("#verify-plex-server").click(function () {
|
||||
if (!(pms_verified)) {
|
||||
var pms_ip = $("#pms_ip").val().trim();
|
||||
var pms_port = $("#pms_port").val().trim();
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").val();
|
||||
var pms_is_remote = $("#pms_is_remote").val();
|
||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Verifying server...');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
$.ajax({
|
||||
url: 'get_server_id',
|
||||
data: {
|
||||
hostname: pms_ip,
|
||||
port: pms_port,
|
||||
identifier: pms_identifier,
|
||||
ssl: pms_ssl,
|
||||
remote: pms_is_remote
|
||||
},
|
||||
cache: true,
|
||||
async: true,
|
||||
timeout: 5000,
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Error verifying server: ' + textStatus);
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
},
|
||||
success: function(xhr, status) {
|
||||
var result = xhr;
|
||||
var identifier = result.identifier;
|
||||
if (identifier) {
|
||||
$("#pms_identifier").val(identifier);
|
||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
pms_verified = true;
|
||||
$("#pms_valid").val("valid");
|
||||
} else {
|
||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please enter both fields.');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
}
|
||||
}
|
||||
$(".pms-settings").change(function () {
|
||||
pms_verified = false;
|
||||
$("#pms_valid").val("");
|
||||
$("#pms-verify-status").html("");
|
||||
});
|
||||
});
|
||||
|
||||
$( ".pms-settings" ).change(function() {
|
||||
pms_verified = false;
|
||||
$("#pms_valid").val("");
|
||||
$("#pms-verify-status").html("");
|
||||
});
|
||||
|
||||
function OAuthPreFunction() {
|
||||
$("#pms_token").val('');
|
||||
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Waiting for authentication...').fadeIn('fast');
|
||||
}
|
||||
function OAuthSuccessCallback(authToken) {
|
||||
$("#pms_token").val(authToken);
|
||||
$("#pms-token-status").html('<i class="fa fa-check"></i> Authentication successful.').fadeIn('fast');
|
||||
authenticated = true;
|
||||
getServerOptions(authToken);
|
||||
}
|
||||
function OAuthErrorCallback() {
|
||||
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Error communicating with Plex.tv.').fadeIn('fast');
|
||||
}
|
||||
|
||||
$('#sign-in-plex').click(function() {
|
||||
PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback, OAuthPreFunction);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
% if data:
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy.helpers import grouper, get_img_service
|
||||
import jellypy
|
||||
from jellypy.helpers import grouper, get_img_service
|
||||
|
||||
recently_added = data['recently_added']
|
||||
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
|
||||
if jellypy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
base_url = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'newsletter/'
|
||||
elif preview:
|
||||
base_url = 'newsletter/'
|
||||
else:
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
service = get_img_service(include_self=True)
|
||||
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/'
|
||||
base_url_image = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'image/'
|
||||
elif preview and service and service != 'self-hosted':
|
||||
base_url_image = 'image/'
|
||||
else:
|
||||
@@ -521,7 +521,7 @@
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
@@ -546,7 +546,7 @@
|
||||
<span class="preheader" style="color: transparent;display: none;height: 0;max-height: 0;max-width: 0;opacity: 0;overflow: hidden;mso-hide: all;visibility: hidden;width: 0;">Tautulli Newsletter - ${subject}</span>
|
||||
% if base_url and not preview:
|
||||
<div class="view-full" style="clear: both;color: #282A2D;font-size: 12px;margin-bottom: 10px;text-align: center;width: 100%;"> <!-- IGNORE SAVE -->
|
||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" rel="noreferrer" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||
</div> <!-- IGNORE SAVE -->
|
||||
% endif
|
||||
<table border="0" cellpadding="3" cellspacing="0" class="main" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background: #282A2D;border-radius: 3px;color: #ffffff;">
|
||||
@@ -599,7 +599,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" style="text-decoration: underline;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
|
||||
</a>
|
||||
</td>
|
||||
@@ -610,7 +610,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${movie['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${movie['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -728,7 +728,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" style="text-decoration: underline;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
|
||||
</a>
|
||||
</td>
|
||||
@@ -739,7 +739,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${show['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${show['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -872,7 +872,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" style="text-decoration: underline;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-cover.png'}" width="150" height="150" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
|
||||
</a>
|
||||
</td>
|
||||
@@ -883,7 +883,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${album['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${album['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -986,7 +986,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + video['thumb_hash']) if base_url_image else video['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||
<tr>
|
||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" style="text-decoration: underline;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer" style="text-decoration: underline;">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225" style="border: none;-ms-interpolation-mode: bicubic;max-width: 100%;display: block;">
|
||||
</a>
|
||||
</td>
|
||||
@@ -997,7 +997,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;height: 100%;">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 0.9rem;vertical-align: top;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;border-bottom: 1px solid rgba(255, 255, 255, .1);line-height: 1.2rem;padding: 5px;max-width: 320px;">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" style="text-decoration: none;color: #ffffff;">${video['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer" style="text-decoration: none;color: #ffffff;">${video['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -1087,4 +1087,4 @@
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
% if data:
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy.helpers import grouper, get_img_service
|
||||
import jellypy
|
||||
from jellypy.helpers import grouper, get_img_service
|
||||
|
||||
recently_added = data['recently_added']
|
||||
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
|
||||
if jellypy.CONFIG.NEWSLETTER_SELF_HOSTED and jellypy.CONFIG.HTTP_BASE_URL:
|
||||
base_url = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'newsletter/'
|
||||
elif preview:
|
||||
base_url = 'newsletter/'
|
||||
else:
|
||||
base_url = ''
|
||||
|
||||
service = get_img_service(include_self=True)
|
||||
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
||||
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/'
|
||||
if service == 'self-hosted' and jellypy.CONFIG.HTTP_BASE_URL:
|
||||
base_url_image = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'image/'
|
||||
elif preview and service and service != 'self-hosted':
|
||||
base_url_image = 'image/'
|
||||
else:
|
||||
@@ -521,7 +521,7 @@
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
@@ -546,7 +546,7 @@
|
||||
<span class="preheader">Tautulli Newsletter - ${subject}</span>
|
||||
% if base_url and not preview:
|
||||
<div class="view-full"> <!-- IGNORE SAVE -->
|
||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" rel="noreferrer">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||
</div> <!-- IGNORE SAVE -->
|
||||
% endif
|
||||
<table border="0" cellpadding="3" cellspacing="0" class="main">
|
||||
@@ -599,7 +599,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']})">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
@@ -610,7 +610,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">${movie['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" rel="noreferrer">${movie['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -728,7 +728,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']})">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
@@ -739,7 +739,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">${show['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" rel="noreferrer">${show['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -872,7 +872,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']})">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-cover.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-cover.png'}" width="150" height="150">
|
||||
</a>
|
||||
</td>
|
||||
@@ -883,7 +883,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">${album['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" rel="noreferrer">${album['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -986,7 +986,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + video['thumb_hash']) if base_url_image else video['thumb_url']})">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer">
|
||||
<img class="card-poster-overlay" src="${base_url_image + 'images/newsletter/view-on-plex-poster.png' if base_url_image else 'https://tautulli.com/images/newsletter/view-on-plex-poster.png'}" width="150" height="225">
|
||||
</a>
|
||||
</td>
|
||||
@@ -997,7 +997,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||
<tr>
|
||||
<td class="card-info-title nowrap">
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank">${video['title']}</a>
|
||||
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${video['rating_key']}" title="${video['title']}" target="_blank" rel="noreferrer">${video['title']}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -1087,4 +1087,4 @@
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
@@ -52,9 +52,7 @@ Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
|
||||
GuessMainPID=no
|
||||
Type=forking
|
||||
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --nolaunch
|
||||
User=tautulli
|
||||
Group=tautulli
|
||||
Restart=on-abnormal
|
||||
|
||||
@@ -13,73 +13,45 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import range
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import future.moves.queue as queue
|
||||
import queue
|
||||
import sqlite3
|
||||
import sys
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||
try:
|
||||
import webbrowser
|
||||
|
||||
no_browser = False
|
||||
except ImportError:
|
||||
no_browser = True
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from UniversalAnalytics import Tracker
|
||||
import pytz
|
||||
|
||||
PYTHON2 = sys.version_info[0] == 2
|
||||
|
||||
if PYTHON2:
|
||||
import activity_handler
|
||||
import activity_pinger
|
||||
import common
|
||||
import database
|
||||
import datafactory
|
||||
import helpers
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import newsletters
|
||||
import newsletter_handler
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import plextv
|
||||
import users
|
||||
import versioncheck
|
||||
import web_socket
|
||||
import webstart
|
||||
import config
|
||||
else:
|
||||
from plexpy import activity_handler
|
||||
from plexpy import activity_pinger
|
||||
from plexpy import common
|
||||
from plexpy import database
|
||||
from plexpy import datafactory
|
||||
from plexpy import helpers
|
||||
from plexpy import libraries
|
||||
from plexpy import logger
|
||||
from plexpy import mobile_app
|
||||
from plexpy import newsletters
|
||||
from plexpy import newsletter_handler
|
||||
from plexpy import notification_handler
|
||||
from plexpy import notifiers
|
||||
from plexpy import plextv
|
||||
from plexpy import users
|
||||
from plexpy import versioncheck
|
||||
from plexpy import web_socket
|
||||
from plexpy import webstart
|
||||
from plexpy import config
|
||||
|
||||
from jellypy import activity_handler
|
||||
from jellypy import activity_pinger
|
||||
from jellypy import common
|
||||
from jellypy import database
|
||||
from jellypy import datafactory
|
||||
from jellypy import exporter
|
||||
from jellypy import libraries
|
||||
from jellypy import logger
|
||||
from jellypy import mobile_app
|
||||
from jellypy import newsletters
|
||||
from jellypy import newsletter_handler
|
||||
from jellypy import notification_handler
|
||||
from jellypy import notifiers
|
||||
from jellypy import users
|
||||
from jellypy import versioncheck
|
||||
from jellypy import web_socket
|
||||
from jellypy import webstart
|
||||
from jellypy import config
|
||||
|
||||
PROG_DIR = None
|
||||
FULL_PATH = None
|
||||
@@ -98,6 +70,8 @@ CREATEPID = False
|
||||
PIDFILE = None
|
||||
NOFORK = False
|
||||
DOCKER = False
|
||||
SNAP = False
|
||||
SNAP_MIGRATE = False
|
||||
FROZEN = False
|
||||
|
||||
SCHED = None
|
||||
@@ -172,6 +146,18 @@ def initialize(config_file):
|
||||
if _INITIALIZED:
|
||||
return False
|
||||
|
||||
if SNAP_MIGRATE:
|
||||
snap_common = os.environ['SNAP_COMMON']
|
||||
old_data_dir = os.path.join(snap_common, 'Tautulli')
|
||||
CONFIG.HTTPS_CERT = CONFIG.HTTPS_CERT.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.HTTPS_CERT_CHAIN = CONFIG.HTTPS_CERT_CHAIN.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.HTTPS_KEY = CONFIG.HTTPS_KEY.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.LOG_DIR = CONFIG.LOG_DIR.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.BACKUP_DIR = CONFIG.BACKUP_DIR.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.CACHE_DIR = CONFIG.CACHE_DIR.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.EXPORT_DIR = CONFIG.EXPORT_DIR.replace(old_data_dir, DATA_DIR)
|
||||
CONFIG.NEWSLETTER_DIR = CONFIG.NEWSLETTER_DIR.replace(old_data_dir, DATA_DIR)
|
||||
|
||||
if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535:
|
||||
logger.warn("HTTP_PORT out of bounds: 21 < %s < 65535", CONFIG.HTTP_PORT)
|
||||
CONFIG.HTTP_PORT = 8181
|
||||
@@ -194,6 +180,8 @@ def initialize(config_file):
|
||||
|
||||
if DOCKER:
|
||||
build = '[Docker] '
|
||||
elif SNAP:
|
||||
build = '[Snap] '
|
||||
elif FROZEN:
|
||||
build = '[Bundle] '
|
||||
else:
|
||||
@@ -226,6 +214,8 @@ def initialize(config_file):
|
||||
CONFIG.BACKUP_DIR, os.path.join(DATA_DIR, 'backups'), 'backups')
|
||||
CONFIG.CACHE_DIR, _ = check_folder_writable(
|
||||
CONFIG.CACHE_DIR, os.path.join(DATA_DIR, 'cache'), 'cache')
|
||||
CONFIG.EXPORT_DIR, _ = check_folder_writable(
|
||||
CONFIG.EXPORT_DIR, os.path.join(DATA_DIR, 'exports'), 'exports')
|
||||
CONFIG.NEWSLETTER_DIR, _ = check_folder_writable(
|
||||
CONFIG.NEWSLETTER_DIR, os.path.join(DATA_DIR, 'newsletters'), 'newsletters')
|
||||
|
||||
@@ -298,7 +288,7 @@ def initialize(config_file):
|
||||
# Check for new versions
|
||||
if CONFIG.CHECK_GITHUB_ON_STARTUP and CONFIG.CHECK_GITHUB:
|
||||
try:
|
||||
versioncheck.check_update()
|
||||
versioncheck.check_update(use_cache=True)
|
||||
except:
|
||||
logger.exception("Unhandled exception")
|
||||
LATEST_VERSION = CURRENT_VERSION
|
||||
@@ -332,18 +322,6 @@ def initialize(config_file):
|
||||
logger.error("Unable to write current release to file '%s': %s" %
|
||||
(release_file, e))
|
||||
|
||||
# Get the real PMS urls for SSL and remote access
|
||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||
plextv.get_server_resources()
|
||||
|
||||
# Refresh the users list on startup
|
||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
||||
users.refresh_users()
|
||||
|
||||
# Refresh the libraries list on startup
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||
libraries.refresh_libraries()
|
||||
|
||||
# Store the original umask
|
||||
UMASK = os.umask(0)
|
||||
os.umask(UMASK)
|
||||
@@ -439,7 +417,7 @@ def initialize_scheduler():
|
||||
|
||||
backup_hours = CONFIG.BACKUP_INTERVAL if 1 <= CONFIG.BACKUP_INTERVAL <= 24 else 6
|
||||
|
||||
schedule_job(database.optimize, 'Optimize Tautulli database',
|
||||
schedule_job(database.optimize_db, 'Optimize Tautulli database',
|
||||
hours=24, minutes=0, seconds=0)
|
||||
schedule_job(database.make_backup, 'Backup Tautulli database',
|
||||
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
||||
@@ -447,8 +425,9 @@ def initialize_scheduler():
|
||||
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
||||
|
||||
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||
hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0)
|
||||
# TODO: Jellyfin
|
||||
# schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||
# hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
|
||||
hours=pms_update_check_hours * bool(CONFIG.MONITOR_PMS_UPDATES), minutes=0, seconds=0)
|
||||
@@ -469,8 +448,9 @@ def initialize_scheduler():
|
||||
|
||||
else:
|
||||
# Cancel all jobs
|
||||
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
# TODO: Jellyfin
|
||||
# schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||
# hours=0, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
@@ -496,7 +476,7 @@ def initialize_scheduler():
|
||||
|
||||
def schedule_job(func, name, hours=0, minutes=0, seconds=0, args=None):
|
||||
"""
|
||||
Start scheduled job if starting or restarting plexpy.
|
||||
Start scheduled job if starting or restarting jellypy.
|
||||
Reschedule job if Interval Settings have changed.
|
||||
Remove job if if Interval Settings changed to 0
|
||||
|
||||
@@ -508,12 +488,16 @@ def schedule_job(func, name, hours=0, minutes=0, seconds=0, args=None):
|
||||
SCHED.remove_job(name)
|
||||
logger.info("Removed background task: %s", name)
|
||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC), args=args)
|
||||
SCHED.reschedule_job(
|
||||
name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC),
|
||||
args=args)
|
||||
logger.info("Re-scheduled background task: %s", name)
|
||||
elif hours > 0 or minutes > 0 or seconds > 0:
|
||||
SCHED.add_job(func, id=name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC), args=args)
|
||||
SCHED.add_job(
|
||||
func, id=name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC),
|
||||
args=args, misfire_grace_time=None)
|
||||
logger.info("Scheduled background task: %s", name)
|
||||
|
||||
|
||||
@@ -521,6 +505,9 @@ def start():
|
||||
global _STARTED
|
||||
|
||||
if _INITIALIZED:
|
||||
# Start refreshes on a separate thread
|
||||
threading.Thread(target=startup_refresh).start()
|
||||
|
||||
global SCHED
|
||||
SCHED = BackgroundScheduler(timezone=pytz.UTC)
|
||||
activity_handler.ACTIVITY_SCHED = BackgroundScheduler(timezone=pytz.UTC)
|
||||
@@ -533,29 +520,36 @@ def start():
|
||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||
notifiers.check_browser_enabled()
|
||||
|
||||
if CONFIG.FIRST_RUN_COMPLETE:
|
||||
activity_pinger.connect_server(log=True, startup=True)
|
||||
|
||||
if CONFIG.SYSTEM_ANALYTICS:
|
||||
global TRACKER
|
||||
TRACKER = initialize_tracker()
|
||||
|
||||
# Send system analytics events
|
||||
if not CONFIG.FIRST_RUN_COMPLETE:
|
||||
analytics_event(category='system', action='install')
|
||||
|
||||
elif _UPDATE:
|
||||
analytics_event(category='system', action='update')
|
||||
|
||||
analytics_event(category='system', action='start')
|
||||
|
||||
# Schedule newsletters
|
||||
newsletter_handler.NEWSLETTER_SCHED.start()
|
||||
newsletter_handler.schedule_newsletters()
|
||||
|
||||
# Cancel processing exports
|
||||
exporter.cancel_exports()
|
||||
|
||||
_STARTED = True
|
||||
|
||||
|
||||
def startup_refresh():
|
||||
# Get the real PMS urls for SSL and remote access
|
||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||
pass
|
||||
# TODO: Jellyfin
|
||||
# plextv.get_server_resources()
|
||||
|
||||
# Connect server after server resource is refreshed
|
||||
if CONFIG.FIRST_RUN_COMPLETE:
|
||||
activity_pinger.connect_server(log=True, startup=True)
|
||||
|
||||
# Refresh the users list on startup
|
||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
||||
users.refresh_users()
|
||||
|
||||
# Refresh the libraries list on startup
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||
libraries.refresh_libraries()
|
||||
|
||||
|
||||
def sig_handler(signum=None, frame=None):
|
||||
if signum is not None:
|
||||
logger.info("Signal %i caught, saving and exiting...", signum)
|
||||
@@ -687,20 +681,21 @@ def dbcheck():
|
||||
'CREATE TABLE IF NOT EXISTS notifiers (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'agent_id INTEGER, agent_name TEXT, agent_label TEXT, friendly_name TEXT, notifier_config TEXT, '
|
||||
'on_play INTEGER DEFAULT 0, on_stop INTEGER DEFAULT 0, on_pause INTEGER DEFAULT 0, '
|
||||
'on_resume INTEGER DEFAULT 0, on_change INTEGER DEFAULT 0, on_buffer INTEGER DEFAULT 0, on_watched INTEGER DEFAULT 0, '
|
||||
'on_created INTEGER DEFAULT 0, on_extdown INTEGER DEFAULT 0, on_intdown INTEGER DEFAULT 0, '
|
||||
'on_resume INTEGER DEFAULT 0, on_change INTEGER DEFAULT 0, on_buffer INTEGER DEFAULT 0, '
|
||||
'on_error INTEGER DEFAULT 0, on_watched INTEGER DEFAULT 0, 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_resume_subject TEXT, on_change_subject TEXT, on_buffer_subject TEXT, on_error_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_resume_body TEXT, on_change_body TEXT, on_buffer_body TEXT, on_error_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, '
|
||||
@@ -772,7 +767,7 @@ def dbcheck():
|
||||
# image_hash_lookup table :: This table keeps record of the image hash lookups
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS image_hash_lookup (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'img_hash TEXT, img TEXT, rating_key INTEGER, width INTEGER, height INTEGER, '
|
||||
'img_hash TEXT UNIQUE, img TEXT, rating_key INTEGER, width INTEGER, height INTEGER, '
|
||||
'opacity INTEGER, background TEXT, blur INTEGER, fallback TEXT)'
|
||||
)
|
||||
|
||||
@@ -788,6 +783,18 @@ def dbcheck():
|
||||
'img_hash TEXT, cloudinary_title TEXT, cloudinary_url TEXT)'
|
||||
)
|
||||
|
||||
# exports table :: This table keeps record of the exported files
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS exports (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'timestamp INTEGER, section_id INTEGER, user_id INTEGER, rating_key INTEGER, media_type TEXT, '
|
||||
'title TEXT, file_format TEXT, '
|
||||
'metadata_level INTEGER, media_info_level INTEGER, '
|
||||
'thumb_level INTEGER DEFAULT 0, art_level INTEGER DEFAULT 0, '
|
||||
'custom_fields TEXT, individual_files INTEGER DEFAULT 0, '
|
||||
'file_size INTEGER DEFAULT 0, complete INTEGER DEFAULT 0, '
|
||||
'exported_items INTEGER DEFAULT 0, total_items INTEGER DEFAULT 0)'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT started FROM sessions')
|
||||
@@ -1942,6 +1949,19 @@ def dbcheck():
|
||||
'ALTER TABLE library_sections ADD COLUMN is_active INTEGER DEFAULT 1'
|
||||
)
|
||||
|
||||
# Upgrade library_sections table from earlier versions
|
||||
try:
|
||||
result = c_db.execute('SELECT thumb, art FROM library_sections WHERE section_id = ?',
|
||||
[common.LIVE_TV_SECTION_ID]).fetchone()
|
||||
if result and (not result[0] or not result[1]):
|
||||
logger.debug("Altering database. Updating database table library_sections.")
|
||||
c_db.execute('UPDATE library_sections SET thumb = ?, art =? WHERE section_id = ?',
|
||||
[common.DEFAULT_LIVE_TV_THUMB,
|
||||
common.DEFAULT_LIVE_TV_ART_FULL,
|
||||
common.LIVE_TV_SECTION_ID])
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# 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()
|
||||
@@ -2012,7 +2032,7 @@ def dbcheck():
|
||||
# Update official mobile device flag
|
||||
for device_id, in c_db.execute('SELECT device_id FROM mobile_devices').fetchall():
|
||||
c_db.execute('UPDATE mobile_devices SET official = ? WHERE device_id = ?',
|
||||
[mobile_app.validate_device_id(device_id), device_id])
|
||||
[mobile_app.validate_onesignal_id(device_id), device_id])
|
||||
|
||||
# Upgrade mobile_devices table from earlier versions
|
||||
try:
|
||||
@@ -2065,6 +2085,21 @@ def dbcheck():
|
||||
'ALTER TABLE notifiers ADD COLUMN on_plexpydbcorrupt_body TEXT'
|
||||
)
|
||||
|
||||
# Upgrade notifiers table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT on_error FROM notifiers')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug("Altering database. Updating database table notifiers.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE notifiers ADD COLUMN on_error INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE notifiers ADD COLUMN on_error_subject TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE notifiers ADD COLUMN on_error_body TEXT'
|
||||
)
|
||||
|
||||
# Upgrade tvmaze_lookup table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT rating_key FROM tvmaze_lookup')
|
||||
@@ -2116,6 +2151,55 @@ def dbcheck():
|
||||
'UPDATE notifiers SET agent_label = "macOS Notification Center" WHERE agent_label = "OSX Notify"'
|
||||
)
|
||||
|
||||
# Upgrade exports table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT thumb_level FROM exports')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug("Altering database. Updating database table exports.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE exports ADD COLUMN thumb_level INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE exports SET thumb_level = 9 WHERE include_thumb = 1'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE exports ADD COLUMN art_level INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE exports SET art_level = 9 WHERE include_art = 1'
|
||||
)
|
||||
|
||||
# Upgrade exports table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT title FROM exports')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug("Altering database. Updating database table exports.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE exports ADD COLUMN title TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE exports ADD COLUMN individual_files INTEGER DEFAULT 0'
|
||||
)
|
||||
|
||||
# Upgrade exports table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT total_items FROM exports')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug("Altering database. Updating database table exports.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE exports ADD COLUMN exported_items INTEGER DEFAULT 0'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE exports ADD COLUMN total_items INTEGER DEFAULT 0'
|
||||
)
|
||||
|
||||
# Upgrade image_hash_lookup table from earlier versions
|
||||
try:
|
||||
c_db.execute('DELETE FROM image_hash_lookup '
|
||||
'WHERE id NOT IN (SELECT MIN(id) FROM image_hash_lookup GROUP BY img_hash)')
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# Add "Local" user to database as default unauthenticated user.
|
||||
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
|
||||
if not result.fetchone():
|
||||
@@ -2174,7 +2258,12 @@ def upgrade():
|
||||
return
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False, checkout=False, reset=False):
|
||||
def shutdown(restart=False, update=False, checkout=False, reset=False,
|
||||
_shutdown=True):
|
||||
if FROZEN and common.PLATFORM == 'Windows' and update:
|
||||
restart = False
|
||||
_shutdown = False
|
||||
|
||||
webstart.stop()
|
||||
|
||||
# Shutdown the websocket connection
|
||||
@@ -2247,61 +2336,21 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
||||
else:
|
||||
logger.info("Tautulli is shutting down...")
|
||||
|
||||
logger.shutdown()
|
||||
if _shutdown:
|
||||
logger.shutdown()
|
||||
|
||||
if WIN_SYS_TRAY_ICON:
|
||||
WIN_SYS_TRAY_ICON.shutdown()
|
||||
elif MAC_SYS_TRAY_ICON:
|
||||
MAC_SYS_TRAY_ICON.shutdown()
|
||||
if WIN_SYS_TRAY_ICON:
|
||||
WIN_SYS_TRAY_ICON.shutdown()
|
||||
elif MAC_SYS_TRAY_ICON:
|
||||
MAC_SYS_TRAY_ICON.shutdown()
|
||||
|
||||
os._exit(0)
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def initialize_tracker():
|
||||
data = {
|
||||
'dataSource': 'server',
|
||||
'appName': common.PRODUCT,
|
||||
'appVersion': common.RELEASE,
|
||||
'appId': INSTALL_TYPE,
|
||||
'appInstallerId': CONFIG.GIT_BRANCH,
|
||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
||||
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
||||
'dimension3': common.PYTHON_VERSION,
|
||||
'userLanguage': SYS_LANGUAGE,
|
||||
'documentEncoding': SYS_ENCODING,
|
||||
'noninteractive': True
|
||||
}
|
||||
|
||||
tracker = Tracker.create('UA-111522699-2', client_id=CONFIG.PMS_UUID, hash_client_id=True,
|
||||
user_agent=common.USER_AGENT)
|
||||
tracker.set(data)
|
||||
|
||||
return tracker
|
||||
|
||||
|
||||
def analytics_event(category, action, label=None, value=None, **kwargs):
|
||||
data = {'category': category, 'action': action}
|
||||
|
||||
if label is not None:
|
||||
data['label'] = label
|
||||
|
||||
if value is not None:
|
||||
data['value'] = value
|
||||
|
||||
if kwargs:
|
||||
data.update(kwargs)
|
||||
|
||||
if TRACKER:
|
||||
try:
|
||||
TRACKER.send('event', data)
|
||||
except Exception as e:
|
||||
logger.warn("Failed to send analytics event for category '%s', action '%s': %s" % (category, action, e))
|
||||
|
||||
|
||||
def check_folder_writable(folder, fallback, name):
|
||||
if not folder:
|
||||
folder = fallback
|
||||
@@ -2326,3 +2375,19 @@ def check_folder_writable(folder, fallback, name):
|
||||
return folder, False
|
||||
|
||||
return folder, True
|
||||
|
||||
|
||||
def get_tautulli_info():
|
||||
tautulli = {
|
||||
'tautulli_install_type': INSTALL_TYPE,
|
||||
'tautulli_version': common.RELEASE,
|
||||
'tautulli_branch': CONFIG.GIT_BRANCH,
|
||||
'tautulli_commit': CURRENT_VERSION,
|
||||
'tautulli_platform': common.PLATFORM,
|
||||
'tautulli_platform_release': common.PLATFORM_RELEASE,
|
||||
'tautulli_platform_version': common.PLATFORM_VERSION,
|
||||
'tautulli_platform_linux_distro': common.PLATFORM_LINUX_DISTRO,
|
||||
'tautulli_platform_device_name': common.PLATFORM_DEVICE_NAME,
|
||||
'tautulli_python_version': common.PYTHON_VERSION,
|
||||
}
|
||||
return tautulli
|
||||
@@ -13,33 +13,20 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
from future.builtins import object
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
import pytz
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import activity_processor
|
||||
import datafactory
|
||||
import helpers
|
||||
import logger
|
||||
import notification_handler
|
||||
import pmsconnect
|
||||
else:
|
||||
from plexpy import activity_processor
|
||||
from plexpy import datafactory
|
||||
from plexpy import helpers
|
||||
from plexpy import logger
|
||||
from plexpy import notification_handler
|
||||
from plexpy import pmsconnect
|
||||
import jellypy
|
||||
|
||||
from jellypy import activity_processor
|
||||
from jellypy import datafactory
|
||||
from jellypy import helpers
|
||||
from jellypy import logger
|
||||
from jellypy import notification_handler
|
||||
|
||||
ACTIVITY_SCHED = None
|
||||
|
||||
@@ -72,27 +59,29 @@ class ActivityHandler(object):
|
||||
|
||||
def get_metadata(self, skip_cache=False):
|
||||
cache_key = None if skip_cache else self.get_session_key()
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key=self.get_rating_key(), cache_key=cache_key)
|
||||
|
||||
if metadata:
|
||||
return metadata
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# metadata = pms_connect.get_metadata_details(rating_key=self.get_rating_key(), cache_key=cache_key)
|
||||
#
|
||||
# if metadata:
|
||||
# return metadata
|
||||
|
||||
return None
|
||||
|
||||
def get_live_session(self, skip_cache=False):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
session_list = pms_connect.get_current_activity(skip_cache=skip_cache)
|
||||
|
||||
if session_list:
|
||||
for session in session_list['sessions']:
|
||||
if int(session['session_key']) == self.get_session_key():
|
||||
# Live sessions don't have rating keys in sessions
|
||||
# Get it from the websocket data
|
||||
if not session['rating_key']:
|
||||
session['rating_key'] = self.get_rating_key()
|
||||
session['rating_key_websocket'] = self.get_rating_key()
|
||||
return session
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# session_list = pms_connect.get_current_activity(skip_cache=skip_cache)
|
||||
#
|
||||
# if session_list:
|
||||
# for session in session_list['sessions']:
|
||||
# if int(session['session_key']) == self.get_session_key():
|
||||
# # Live sessions don't have rating keys in sessions
|
||||
# # Get it from the websocket data
|
||||
# if not session['rating_key']:
|
||||
# session['rating_key'] = self.get_rating_key()
|
||||
# session['rating_key_websocket'] = self.get_rating_key()
|
||||
# return session
|
||||
|
||||
return None
|
||||
|
||||
@@ -134,7 +123,7 @@ class ActivityHandler(object):
|
||||
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
|
||||
|
||||
# Send notification after updating db
|
||||
#plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||
# jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||
|
||||
# Write the new session to our temp session table
|
||||
self.update_db_session(session=session, notify=True)
|
||||
@@ -162,7 +151,7 @@ class ActivityHandler(object):
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
|
||||
|
||||
# Write it to the history table
|
||||
monitor_proc = activity_processor.ActivityProcessor()
|
||||
@@ -198,7 +187,7 @@ class ActivityHandler(object):
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
if not still_paused:
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
|
||||
|
||||
def on_resume(self):
|
||||
if self.is_valid_session():
|
||||
@@ -214,11 +203,12 @@ class ActivityHandler(object):
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
|
||||
|
||||
def on_change(self):
|
||||
if self.is_valid_session():
|
||||
logger.debug("Tautulli ActivityHandler :: Session %s has changed transcode decision." % str(self.get_session_key()))
|
||||
logger.debug(
|
||||
"Tautulli ActivityHandler :: Session %s has changed transcode decision." % str(self.get_session_key()))
|
||||
|
||||
# Update the session state and viewOffset
|
||||
self.update_db_session()
|
||||
@@ -227,7 +217,7 @@ class ActivityHandler(object):
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_change'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_change'})
|
||||
|
||||
def on_buffer(self):
|
||||
if self.is_valid_session():
|
||||
@@ -255,14 +245,27 @@ class ActivityHandler(object):
|
||||
(self.get_session_key(), buffer_last_triggered))
|
||||
time_since_last_trigger = helpers.timestamp() - int(buffer_last_triggered)
|
||||
|
||||
if current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger == 0 or \
|
||||
time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT:
|
||||
if current_buffer_count >= jellypy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger == 0 or \
|
||||
time_since_last_trigger >= jellypy.CONFIG.BUFFER_WAIT:
|
||||
ap.set_session_buffer_trigger_time(session_key=self.get_session_key())
|
||||
|
||||
# Retrieve the session data from our temp table
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
|
||||
|
||||
def on_error(self):
|
||||
if self.is_valid_session():
|
||||
logger.debug("Tautulli ActivityHandler :: Session %s encountered an error." % str(self.get_session_key()))
|
||||
|
||||
# Update the session state and viewOffset
|
||||
self.update_db_session()
|
||||
|
||||
# Retrieve the session data from our temp table
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_error'})
|
||||
|
||||
# This function receives events from our websocket connection
|
||||
def process(self):
|
||||
@@ -303,8 +306,8 @@ class ActivityHandler(object):
|
||||
|
||||
# Make sure the same item is being played
|
||||
if (this_rating_key == last_rating_key
|
||||
or this_rating_key == last_rating_key_websocket
|
||||
or this_live_uuid == last_live_uuid) \
|
||||
or this_rating_key == last_rating_key_websocket
|
||||
or this_live_uuid == last_live_uuid) \
|
||||
and this_guid == last_guid:
|
||||
# Update the session state and viewOffset
|
||||
if this_state == 'playing':
|
||||
@@ -321,6 +324,8 @@ class ActivityHandler(object):
|
||||
self.on_resume()
|
||||
elif this_state == 'stopped':
|
||||
self.on_stop()
|
||||
elif this_state == 'error':
|
||||
self.on_error()
|
||||
|
||||
elif this_state == 'paused':
|
||||
# Update the session last_paused timestamp
|
||||
@@ -343,10 +348,10 @@ class ActivityHandler(object):
|
||||
# The only purpose of this is for notifications
|
||||
if not db_session['watched'] and this_state != 'buffering':
|
||||
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||
watched_percent = {'movie': jellypy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': jellypy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': jellypy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||
'clip': jellypy.CONFIG.TV_WATCHED_PERCENT
|
||||
}
|
||||
|
||||
if progress_percent >= watched_percent.get(db_session['media_type'], 101):
|
||||
@@ -358,9 +363,9 @@ class ActivityHandler(object):
|
||||
session=db_session, notify_action='on_watched', notified=False)
|
||||
|
||||
for d in watched_notifiers:
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
||||
'notifier_id': d['notifier_id'],
|
||||
'notify_action': 'on_watched'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
||||
'notifier_id': d['notifier_id'],
|
||||
'notify_action': 'on_watched'})
|
||||
|
||||
else:
|
||||
# We don't have this session in our table yet, start a new one.
|
||||
@@ -386,11 +391,12 @@ class TimelineHandler(object):
|
||||
return None
|
||||
|
||||
def get_metadata(self):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(self.get_rating_key())
|
||||
|
||||
if metadata:
|
||||
return metadata
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# metadata = pms_connect.get_metadata_details(self.get_rating_key())
|
||||
#
|
||||
# if metadata:
|
||||
# return metadata
|
||||
|
||||
return None
|
||||
|
||||
@@ -424,9 +430,9 @@ class TimelineHandler(object):
|
||||
|
||||
# Add a new media item to the recently added queue
|
||||
if media_type and section_id > 0 and \
|
||||
((state_type == 0 and metadata_state == 'created')): # or \
|
||||
#(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \
|
||||
#media_state == 'analyzing' and queue_size is None)):
|
||||
((state_type == 0 and metadata_state == 'created')): # or \
|
||||
# (jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \
|
||||
# media_state == 'analyzing' and queue_size is None)):
|
||||
|
||||
if media_type in ('episode', 'track'):
|
||||
metadata = self.get_metadata()
|
||||
@@ -445,14 +451,15 @@ class TimelineHandler(object):
|
||||
|
||||
RECENTLY_ADDED_QUEUE[rating_key] = set([grandparent_rating_key])
|
||||
|
||||
logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s, grandparent %s) added to recently added queue."
|
||||
% (title, str(rating_key), str(grandparent_rating_key)))
|
||||
logger.debug(
|
||||
"Tautulli TimelineHandler :: Library item '%s' (%s, grandparent %s) added to recently added queue."
|
||||
% (title, str(rating_key), str(grandparent_rating_key)))
|
||||
|
||||
# Schedule a callback to clear the recently added queue
|
||||
schedule_callback('rating_key-{}'.format(grandparent_rating_key),
|
||||
func=clear_recently_added_queue,
|
||||
args=[grandparent_rating_key, grandparent_title],
|
||||
seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||
seconds=jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||
|
||||
elif media_type in ('season', 'album'):
|
||||
metadata = self.get_metadata()
|
||||
@@ -464,14 +471,15 @@ class TimelineHandler(object):
|
||||
parent_set.add(rating_key)
|
||||
RECENTLY_ADDED_QUEUE[parent_rating_key] = parent_set
|
||||
|
||||
logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s , parent %s) added to recently added queue."
|
||||
% (title, str(rating_key), str(parent_rating_key)))
|
||||
logger.debug(
|
||||
"Tautulli TimelineHandler :: Library item '%s' (%s , parent %s) added to recently added queue."
|
||||
% (title, str(rating_key), str(parent_rating_key)))
|
||||
|
||||
# Schedule a callback to clear the recently added queue
|
||||
schedule_callback('rating_key-{}'.format(parent_rating_key),
|
||||
func=clear_recently_added_queue,
|
||||
args=[parent_rating_key, parent_title],
|
||||
seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||
seconds=jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||
|
||||
else:
|
||||
queue_set = RECENTLY_ADDED_QUEUE.get(rating_key, set())
|
||||
@@ -484,12 +492,12 @@ class TimelineHandler(object):
|
||||
schedule_callback('rating_key-{}'.format(rating_key),
|
||||
func=clear_recently_added_queue,
|
||||
args=[rating_key, title],
|
||||
seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||
seconds=jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||
|
||||
# A movie, show, or artist is done processing
|
||||
elif media_type in ('movie', 'show', 'artist') and section_id > 0 and \
|
||||
state_type == 5 and metadata_state is None and queue_size is None and \
|
||||
rating_key in RECENTLY_ADDED_QUEUE:
|
||||
state_type == 5 and metadata_state is None and queue_size is None and \
|
||||
rating_key in RECENTLY_ADDED_QUEUE:
|
||||
|
||||
logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s) done processing metadata."
|
||||
% (title, str(rating_key)))
|
||||
@@ -516,9 +524,17 @@ class ReachabilityHandler(object):
|
||||
return False
|
||||
|
||||
def remote_access_enabled(self):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
pref = pms_connect.get_server_pref(pref='PublishServerOnPlexOnlineKey')
|
||||
return helpers.bool_true(pref)
|
||||
return False
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# pref = pms_connect.get_server_pref(pref='PublishServerOnPlexOnlineKey')
|
||||
# return helpers.bool_true(pref)
|
||||
|
||||
def on_down(self, server_response):
|
||||
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
|
||||
|
||||
def on_up(self, server_response):
|
||||
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
|
||||
|
||||
def process(self):
|
||||
# Check if remote access is enabled
|
||||
@@ -526,32 +542,43 @@ class ReachabilityHandler(object):
|
||||
return
|
||||
|
||||
# Do nothing if remote access is still up and hasn't changed
|
||||
if self.is_reachable() and plexpy.PLEX_REMOTE_ACCESS_UP:
|
||||
if self.is_reachable() and jellypy.PLEX_REMOTE_ACCESS_UP:
|
||||
return
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
server_response = pms_connect.get_server_response()
|
||||
|
||||
if server_response:
|
||||
# Waiting for port mapping
|
||||
if server_response['mapping_state'] == 'waiting':
|
||||
logger.warn("Tautulli Monitor :: Remote access waiting for port mapping.")
|
||||
|
||||
elif plexpy.PLEX_REMOTE_ACCESS_UP is not False and server_response['reason']:
|
||||
logger.warn("Tautulli Monitor :: Remote access failed: %s" % server_response['reason'])
|
||||
logger.info("Tautulli Monitor :: Plex remote access is down.")
|
||||
|
||||
plexpy.PLEX_REMOTE_ACCESS_UP = False
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
|
||||
|
||||
elif plexpy.PLEX_REMOTE_ACCESS_UP is False and not server_response['reason']:
|
||||
logger.info("Tautulli Monitor :: Plex remote access is back up.")
|
||||
|
||||
plexpy.PLEX_REMOTE_ACCESS_UP = True
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
|
||||
|
||||
elif plexpy.PLEX_REMOTE_ACCESS_UP is None:
|
||||
plexpy.PLEX_REMOTE_ACCESS_UP = self.is_reachable()
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# server_response = pms_connect.get_server_response()
|
||||
#
|
||||
# if server_response:
|
||||
# # Waiting for port mapping
|
||||
# if server_response['mapping_state'] == 'waiting':
|
||||
# logger.warn("Tautulli ReachabilityHandler :: Remote access waiting for port mapping.")
|
||||
#
|
||||
# elif jellypy.PLEX_REMOTE_ACCESS_UP is not False and server_response['reason']:
|
||||
# logger.warn("Tautulli ReachabilityHandler :: Remote access failed: %s" % server_response['reason'])
|
||||
# logger.info("Tautulli ReachabilityHandler :: Plex remote access is down.")
|
||||
#
|
||||
# jellypy.PLEX_REMOTE_ACCESS_UP = False
|
||||
#
|
||||
# if not ACTIVITY_SCHED.get_job('on_extdown'):
|
||||
# logger.debug("Tautulli ReachabilityHandler :: Schedule remote access down callback in %d seconds.",
|
||||
# jellypy.CONFIG.NOTIFY_REMOTE_ACCESS_THRESHOLD)
|
||||
# schedule_callback('on_extdown', func=self.on_down, args=[server_response],
|
||||
# seconds=jellypy.CONFIG.NOTIFY_REMOTE_ACCESS_THRESHOLD)
|
||||
#
|
||||
# elif jellypy.PLEX_REMOTE_ACCESS_UP is False and not server_response['reason']:
|
||||
# logger.info("Tautulli ReachabilityHandler :: Plex remote access is back up.")
|
||||
#
|
||||
# jellypy.PLEX_REMOTE_ACCESS_UP = True
|
||||
#
|
||||
# if ACTIVITY_SCHED.get_job('on_extdown'):
|
||||
# logger.debug("Tautulli ReachabilityHandler :: Cancelling scheduled remote access down callback.")
|
||||
# schedule_callback('on_extdown', remove_job=True)
|
||||
# else:
|
||||
# self.on_up(server_response)
|
||||
#
|
||||
# elif jellypy.PLEX_REMOTE_ACCESS_UP is None:
|
||||
# jellypy.PLEX_REMOTE_ACCESS_UP = self.is_reachable()
|
||||
|
||||
|
||||
def del_keys(key):
|
||||
@@ -575,7 +602,8 @@ def schedule_callback(id, func=None, remove_job=False, args=None, **kwargs):
|
||||
ACTIVITY_SCHED.add_job(
|
||||
func, args=args, id=id, trigger=DateTrigger(
|
||||
run_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(**kwargs),
|
||||
timezone=pytz.UTC))
|
||||
timezone=pytz.UTC),
|
||||
misfire_grace_time=None)
|
||||
|
||||
|
||||
def force_stop_stream(session_key, title, user):
|
||||
@@ -586,18 +614,20 @@ def force_stop_stream(session_key, title, user):
|
||||
|
||||
if row_id:
|
||||
# If session is written to the database successfully, remove the session from the session table
|
||||
logger.info("Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (session['session_key'], session['rating_key']))
|
||||
logger.info(
|
||||
"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (session['session_key'], session['rating_key']))
|
||||
ap.delete_session(row_id=row_id)
|
||||
delete_metadata_cache(session_key)
|
||||
|
||||
else:
|
||||
session['write_attempts'] += 1
|
||||
|
||||
if session['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||
logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again in 30 seconds. Write attempt %s."
|
||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||
if session['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||
logger.warn(
|
||||
"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again in 30 seconds. Write attempt %s."
|
||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||
ap.increment_write_attempts(session_key=session_key)
|
||||
|
||||
# Reschedule for 30 seconds later
|
||||
@@ -605,11 +635,13 @@ def force_stop_stream(session_key, title, user):
|
||||
args=[session_key, session['full_title'], session['user']], seconds=30)
|
||||
|
||||
else:
|
||||
logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
"Removing session from the database. Write attempt %s."
|
||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||
logger.info("Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (session['session_key'], session['rating_key']))
|
||||
logger.warn(
|
||||
"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
"Removing session from the database. Write attempt %s."
|
||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||
logger.info(
|
||||
"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (session['session_key'], session['rating_key']))
|
||||
ap.delete_session(session_key=session_key)
|
||||
delete_metadata_cache(session_key)
|
||||
|
||||
@@ -617,14 +649,14 @@ def force_stop_stream(session_key, title, user):
|
||||
def clear_recently_added_queue(rating_key, title):
|
||||
child_keys = RECENTLY_ADDED_QUEUE[rating_key]
|
||||
|
||||
if plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT and len(child_keys) > 1:
|
||||
if jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT and len(child_keys) > 1:
|
||||
on_created(rating_key, child_keys=child_keys)
|
||||
|
||||
elif child_keys:
|
||||
for child_key in child_keys:
|
||||
grandchild_keys = RECENTLY_ADDED_QUEUE.get(child_key, [])
|
||||
|
||||
if plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT and len(grandchild_keys) > 1:
|
||||
if jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT and len(grandchild_keys) > 1:
|
||||
on_created(child_key, child_keys=grandchild_keys)
|
||||
|
||||
elif grandchild_keys:
|
||||
@@ -643,46 +675,47 @@ def clear_recently_added_queue(rating_key, title):
|
||||
|
||||
def on_created(rating_key, **kwargs):
|
||||
logger.debug("Tautulli TimelineHandler :: Library item %s added to Plex." % str(rating_key))
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key)
|
||||
|
||||
if metadata:
|
||||
notify = True
|
||||
# now = helpers.timestamp()
|
||||
#
|
||||
# if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
|
||||
# logger.debug("Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
|
||||
# % str(rating_key))
|
||||
# notify = False
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
if 'child_keys' not in kwargs:
|
||||
if data_factory.get_recently_added_item(rating_key):
|
||||
logger.debug("Tautulli TimelineHandler :: Library item %s added already. Not notifying again."
|
||||
% str(rating_key))
|
||||
notify = False
|
||||
|
||||
if notify:
|
||||
data = {'timeline_data': metadata, 'notify_action': 'on_created'}
|
||||
data.update(kwargs)
|
||||
plexpy.NOTIFY_QUEUE.put(data)
|
||||
|
||||
all_keys = [rating_key]
|
||||
if 'child_keys' in kwargs:
|
||||
all_keys.extend(kwargs['child_keys'])
|
||||
|
||||
for key in all_keys:
|
||||
data_factory.set_recently_added_item(key)
|
||||
|
||||
logger.debug("Added %s items to the recently_added database table." % str(len(all_keys)))
|
||||
|
||||
else:
|
||||
logger.error("Tautulli TimelineHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# metadata = pms_connect.get_metadata_details(rating_key)
|
||||
#
|
||||
# if metadata:
|
||||
# notify = True
|
||||
# # now = helpers.timestamp()
|
||||
# #
|
||||
# # if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
|
||||
# # logger.debug("Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
|
||||
# # % str(rating_key))
|
||||
# # notify = False
|
||||
#
|
||||
# data_factory = datafactory.DataFactory()
|
||||
# if 'child_keys' not in kwargs:
|
||||
# if data_factory.get_recently_added_item(rating_key):
|
||||
# logger.debug("Tautulli TimelineHandler :: Library item %s added already. Not notifying again."
|
||||
# % str(rating_key))
|
||||
# notify = False
|
||||
#
|
||||
# if notify:
|
||||
# data = {'timeline_data': metadata, 'notify_action': 'on_created'}
|
||||
# data.update(kwargs)
|
||||
# jellypy.NOTIFY_QUEUE.put(data)
|
||||
#
|
||||
# all_keys = [rating_key]
|
||||
# if 'child_keys' in kwargs:
|
||||
# all_keys.extend(kwargs['child_keys'])
|
||||
#
|
||||
# for key in all_keys:
|
||||
# data_factory.set_recently_added_item(key)
|
||||
#
|
||||
# logger.debug("Added %s items to the recently_added database table." % str(len(all_keys)))
|
||||
#
|
||||
# else:
|
||||
# logger.error("Tautulli TimelineHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
|
||||
|
||||
|
||||
def delete_metadata_cache(session_key):
|
||||
try:
|
||||
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % session_key))
|
||||
os.remove(os.path.join(jellypy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % session_key))
|
||||
except OSError as e:
|
||||
logger.error("Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
|
||||
% (session_key, e))
|
||||
@@ -13,35 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
|
||||
import threading
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import activity_handler
|
||||
import activity_processor
|
||||
import database
|
||||
import helpers
|
||||
import libraries
|
||||
import logger
|
||||
import notification_handler
|
||||
import plextv
|
||||
import pmsconnect
|
||||
import web_socket
|
||||
else:
|
||||
from plexpy import activity_handler
|
||||
from plexpy import activity_processor
|
||||
from plexpy import database
|
||||
from plexpy import helpers
|
||||
from plexpy import libraries
|
||||
from plexpy import logger
|
||||
from plexpy import notification_handler
|
||||
from plexpy import plextv
|
||||
from plexpy import pmsconnect
|
||||
from plexpy import web_socket
|
||||
import jellypy
|
||||
|
||||
from jellypy import activity_handler
|
||||
from jellypy import activity_processor
|
||||
from jellypy import database
|
||||
from jellypy import helpers
|
||||
from jellypy import logger
|
||||
from jellypy import notification_handler
|
||||
from jellypy import web_socket
|
||||
|
||||
monitor_lock = threading.Lock()
|
||||
ext_ping_count = 0
|
||||
@@ -50,7 +32,6 @@ int_ping_count = 0
|
||||
|
||||
|
||||
def check_active_sessions(ws_request=False):
|
||||
|
||||
with monitor_lock:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
monitor_process = activity_processor.ActivityProcessor()
|
||||
@@ -60,8 +41,10 @@ def check_active_sessions(ws_request=False):
|
||||
for stream in db_streams:
|
||||
activity_handler.delete_metadata_cache(stream['session_key'])
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
session_list = pms_connect.get_current_activity()
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# session_list = pms_connect.get_current_activity()
|
||||
session_list = None
|
||||
|
||||
logger.debug("Tautulli Monitor :: Checking for active streams.")
|
||||
|
||||
@@ -82,23 +65,32 @@ def check_active_sessions(ws_request=False):
|
||||
if session['state'] == 'paused':
|
||||
logger.debug("Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_pause'})
|
||||
|
||||
if session['state'] == 'playing' and stream['state'] == 'paused':
|
||||
logger.debug("Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_resume'})
|
||||
|
||||
if session['state'] == 'error':
|
||||
logger.debug(
|
||||
"Tautulli Monitor :: Session %s encountered an error." % stream['session_key'])
|
||||
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_error'})
|
||||
|
||||
if stream['state'] == 'paused' and not ws_request:
|
||||
# The stream is still paused so we need to increment the paused_counter
|
||||
# Using the set config parameter as the interval, probably not the most accurate but
|
||||
# it will have to do for now. If it's a websocket request don't use this method.
|
||||
paused_counter = int(stream['paused_counter']) + plexpy.CONFIG.MONITORING_INTERVAL
|
||||
paused_counter = int(stream['paused_counter']) + jellypy.CONFIG.MONITORING_INTERVAL
|
||||
monitor_db.action('UPDATE sessions SET paused_counter = ? '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[paused_counter, stream['session_key'], stream['rating_key']])
|
||||
|
||||
if session['state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0:
|
||||
if session['state'] == 'buffering' and jellypy.CONFIG.BUFFER_THRESHOLD > 0:
|
||||
# The stream is buffering so we need to increment the buffer_count
|
||||
# We're going just increment on every monitor ping,
|
||||
# would be difficult to keep track otherwise
|
||||
@@ -112,11 +104,11 @@ def check_active_sessions(ws_request=False):
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
if buffer_values[0]['buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD:
|
||||
if buffer_values[0]['buffer_count'] >= jellypy.CONFIG.BUFFER_THRESHOLD:
|
||||
# Push any notifications -
|
||||
# Push it on it's own thread so we don't hold up our db actions
|
||||
# Our first buffer notification
|
||||
if buffer_values[0]['buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD:
|
||||
if buffer_values[0]['buffer_count'] == jellypy.CONFIG.BUFFER_THRESHOLD:
|
||||
logger.info("Tautulli Monitor :: User '%s' has triggered a buffer warning."
|
||||
% stream['user'])
|
||||
# Set the buffer trigger time
|
||||
@@ -125,26 +117,30 @@ def check_active_sessions(ws_request=False):
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||
|
||||
else:
|
||||
# Subsequent buffer notifications after wait time
|
||||
if helpers.timestamp() > buffer_values[0]['buffer_last_triggered'] + \
|
||||
plexpy.CONFIG.BUFFER_WAIT:
|
||||
logger.info("Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
|
||||
% stream['user'])
|
||||
jellypy.CONFIG.BUFFER_WAIT:
|
||||
logger.info(
|
||||
"Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
|
||||
% stream['user'])
|
||||
# Set the buffer trigger time
|
||||
monitor_db.action('UPDATE sessions '
|
||||
'SET buffer_last_triggered = strftime("%s","now") '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||
|
||||
logger.debug("Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||
% (stream['session_key'],
|
||||
buffer_values[0]['buffer_count'],
|
||||
buffer_values[0]['buffer_last_triggered']))
|
||||
logger.debug(
|
||||
"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||
% (stream['session_key'],
|
||||
buffer_values[0]['buffer_count'],
|
||||
buffer_values[0]['buffer_last_triggered']))
|
||||
|
||||
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
||||
# Don't trigger if state is buffer as some clients push the progress to the end when
|
||||
@@ -152,11 +148,15 @@ def check_active_sessions(ws_request=False):
|
||||
if session['state'] != 'buffering':
|
||||
progress_percent = helpers.get_percent(session['view_offset'], session['duration'])
|
||||
notify_states = notification_handler.get_notify_state(session=session)
|
||||
if (session['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||
if (session[
|
||||
'media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||
session[
|
||||
'media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
|
||||
session[
|
||||
'media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||
|
||||
else:
|
||||
# The user has stopped playing a stream
|
||||
@@ -168,17 +168,21 @@ def check_active_sessions(ws_request=False):
|
||||
stream['stopped'] = helpers.timestamp()
|
||||
monitor_db.action('UPDATE sessions SET stopped = ?, state = ? '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['stopped'], 'stopped', stream['session_key'], stream['rating_key']])
|
||||
[stream['stopped'], 'stopped', stream['session_key'],
|
||||
stream['rating_key']])
|
||||
|
||||
progress_percent = helpers.get_percent(stream['view_offset'], stream['duration'])
|
||||
notify_states = notification_handler.get_notify_state(session=stream)
|
||||
if (stream['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||
if (stream[
|
||||
'media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||
stream[
|
||||
'media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
|
||||
stream[
|
||||
'media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
|
||||
|
||||
# Write the item history on playback stop
|
||||
row_id = monitor_process.write_session_history(session=stream)
|
||||
@@ -191,15 +195,17 @@ def check_active_sessions(ws_request=False):
|
||||
else:
|
||||
stream['write_attempts'] += 1
|
||||
|
||||
if stream['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||
logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again on the next pass. Write attempt %s."
|
||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||
if stream['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||
logger.warn(
|
||||
"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again on the next pass. Write attempt %s."
|
||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||
monitor_process.increment_write_attempts(session_key=stream['session_key'])
|
||||
else:
|
||||
logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Removing session from the database. Write attempt %s."
|
||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||
logger.warn(
|
||||
"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Removing session from the database. Write attempt %s."
|
||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||
logger.debug("Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (stream['session_key'], stream['rating_key']))
|
||||
monitor_process.delete_session(session_key=stream['session_key'])
|
||||
@@ -209,20 +215,24 @@ def check_active_sessions(ws_request=False):
|
||||
new_session = monitor_process.write_session(session)
|
||||
|
||||
if new_session:
|
||||
logger.debug("Tautulli Monitor :: Session %s started by user %s with ratingKey %s."
|
||||
% (session['session_key'], session['user_id'], session['rating_key']))
|
||||
logger.debug("Tautulli Monitor :: Session %s started by user %s (%s) with ratingKey %s (%s)%s."
|
||||
% (str(session['session_key']), str(session['user_id']), session['username'],
|
||||
str(session['rating_key']), session['full_title'],
|
||||
'[Live TV]' if session['live'] else ''))
|
||||
|
||||
else:
|
||||
logger.debug("Tautulli Monitor :: Unable to read session list.")
|
||||
|
||||
|
||||
def connect_server(log=True, startup=False):
|
||||
if plexpy.CONFIG.PMS_IS_CLOUD:
|
||||
if jellypy.CONFIG.PMS_IS_CLOUD:
|
||||
if log:
|
||||
logger.info("Tautulli Monitor :: Checking for Plex Cloud server status...")
|
||||
|
||||
plex_tv = plextv.PlexTV()
|
||||
status = plex_tv.get_cloud_server_status()
|
||||
# TODO: Jellyfin
|
||||
# plex_tv = plextv.PlexTV()
|
||||
# status = plex_tv.get_cloud_server_status()
|
||||
status = None
|
||||
|
||||
if status is True:
|
||||
logger.info("Tautulli Monitor :: Plex Cloud server is active.")
|
||||
@@ -250,20 +260,20 @@ def connect_server(log=True, startup=False):
|
||||
|
||||
|
||||
def check_server_updates():
|
||||
|
||||
with monitor_lock:
|
||||
logger.info("Tautulli Monitor :: Checking for PMS updates...")
|
||||
|
||||
plex_tv = plextv.PlexTV()
|
||||
download_info = plex_tv.get_plex_downloads()
|
||||
|
||||
if download_info:
|
||||
logger.info("Tautulli Monitor :: Current PMS version: %s", plexpy.CONFIG.PMS_VERSION)
|
||||
|
||||
if download_info['update_available']:
|
||||
logger.info("Tautulli Monitor :: PMS update available version: %s", download_info['version'])
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
||||
|
||||
else:
|
||||
logger.info("Tautulli Monitor :: No PMS update available.")
|
||||
# TODO: Jellyfin
|
||||
# plex_tv = plextv.PlexTV()
|
||||
# download_info = plex_tv.get_plex_downloads()
|
||||
#
|
||||
# if download_info:
|
||||
# logger.info("Tautulli Monitor :: Current PMS version: %s", jellypy.CONFIG.PMS_VERSION)
|
||||
#
|
||||
# if download_info['update_available']:
|
||||
# logger.info("Tautulli Monitor :: PMS update available version: %s", download_info['version'])
|
||||
#
|
||||
# jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
||||
#
|
||||
# else:
|
||||
# logger.info("Tautulli Monitor :: No PMS update available.")
|
||||
@@ -13,28 +13,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
from future.builtins import object
|
||||
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import database
|
||||
import helpers
|
||||
import libraries
|
||||
import logger
|
||||
import pmsconnect
|
||||
import users
|
||||
else:
|
||||
from plexpy import database
|
||||
from plexpy import helpers
|
||||
from plexpy import libraries
|
||||
from plexpy import logger
|
||||
from plexpy import pmsconnect
|
||||
from plexpy import users
|
||||
import jellypy
|
||||
|
||||
from jellypy import database
|
||||
from jellypy import helpers
|
||||
from jellypy import libraries
|
||||
from jellypy import logger
|
||||
from jellypy import users
|
||||
|
||||
|
||||
class ActivityProcessor(object):
|
||||
@@ -165,7 +153,7 @@ class ActivityProcessor(object):
|
||||
# Check if any notification agents have notifications enabled
|
||||
if notify:
|
||||
session.update(timestamp)
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||
jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||
|
||||
# Add Live TV library if it hasn't been added
|
||||
if values['live']:
|
||||
@@ -229,16 +217,17 @@ class ActivityProcessor(object):
|
||||
(session['session_key'], session['rating_key'], session['media_type']))
|
||||
return session['id']
|
||||
|
||||
real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(session['paused_counter'])
|
||||
real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(
|
||||
session['paused_counter'])
|
||||
|
||||
if not is_import and plexpy.CONFIG.LOGGING_IGNORE_INTERVAL:
|
||||
if not is_import and jellypy.CONFIG.LOGGING_IGNORE_INTERVAL:
|
||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||
(real_play_time < int(jellypy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||
logging_enabled = False
|
||||
logger.debug("Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
|
||||
"which is less than %s seconds, so we're not logging it." %
|
||||
(session['session_key'], session['rating_key'], str(real_play_time),
|
||||
plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||
jellypy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||
if not is_import and session['media_type'] == 'track':
|
||||
if real_play_time < 15 and helpers.cast_to_int(session['duration']) >= 30:
|
||||
logging_enabled = False
|
||||
@@ -249,35 +238,41 @@ class ActivityProcessor(object):
|
||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||
(real_play_time < int(import_ignore_interval)):
|
||||
logging_enabled = False
|
||||
logger.debug("Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||
"seconds, so we're not logging it." %
|
||||
(session['rating_key'], str(real_play_time), import_ignore_interval))
|
||||
logger.debug(
|
||||
"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||
"seconds, so we're not logging it." %
|
||||
(session['rating_key'], str(real_play_time), import_ignore_interval))
|
||||
|
||||
if not is_import and not user_details['keep_history']:
|
||||
logging_enabled = False
|
||||
logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details['username'])
|
||||
logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details[
|
||||
'username'])
|
||||
elif not is_import and not library_details['keep_history']:
|
||||
logging_enabled = False
|
||||
logger.debug("Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details['section_name'])
|
||||
logger.debug(
|
||||
"Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details[
|
||||
'section_name'])
|
||||
|
||||
if logging_enabled:
|
||||
|
||||
# Fetch metadata first so we can return false if it fails
|
||||
if not is_import:
|
||||
logger.debug("Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
if session['live']:
|
||||
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']),
|
||||
cache_key=session['session_key'],
|
||||
return_cache=True)
|
||||
else:
|
||||
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||
if not metadata:
|
||||
return False
|
||||
else:
|
||||
media_info = {}
|
||||
if 'media_info' in metadata and len(metadata['media_info']) > 0:
|
||||
media_info = metadata['media_info'][0]
|
||||
logger.debug(
|
||||
"Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# if session['live']:
|
||||
# metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']),
|
||||
# cache_key=session['session_key'],
|
||||
# return_cache=True)
|
||||
# else:
|
||||
# metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||
# if not metadata:
|
||||
# return False
|
||||
# else:
|
||||
# media_info = {}
|
||||
# if 'media_info' in metadata and len(metadata['media_info']) > 0:
|
||||
# media_info = metadata['media_info'][0]
|
||||
else:
|
||||
metadata = import_metadata
|
||||
## TODO: Fix media info from imports. Temporary media info from import session.
|
||||
@@ -360,9 +355,9 @@ class ActivityProcessor(object):
|
||||
'view_offset': result[1]['view_offset'],
|
||||
'reference_id': result[1]['reference_id']}
|
||||
|
||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||
watched_percent = {'movie': jellypy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': jellypy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': jellypy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||
}
|
||||
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
|
||||
media_watched_percent = watched_percent.get(session['media_type'], 0)
|
||||
@@ -383,7 +378,7 @@ class ActivityProcessor(object):
|
||||
args = [new_session['id'], new_session['id']]
|
||||
|
||||
self.db.action(query=query, args=args)
|
||||
|
||||
|
||||
# logger.debug("Tautulli ActivityProcessor :: Successfully written history item, last id for session_history is %s"
|
||||
# % last_id)
|
||||
|
||||
@@ -661,4 +656,4 @@ class ActivityProcessor(object):
|
||||
'WHERE user_id = ? AND machine_id = ? AND media_type = ? '
|
||||
'ORDER BY stopped DESC',
|
||||
[user_id, machine_id, media_type])
|
||||
return int(started - last_session.get('stopped', 0) >= plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)
|
||||
return int(started - last_session.get('stopped', 0) >= jellypy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)
|
||||
@@ -15,14 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
from future.builtins import object
|
||||
|
||||
from hashing_passwords import check_hash
|
||||
from io import open
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
@@ -31,35 +23,25 @@ import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from io import open
|
||||
|
||||
import cherrypy
|
||||
import xmltodict
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import config
|
||||
import database
|
||||
import helpers
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import newsletter_handler
|
||||
import newsletters
|
||||
import users
|
||||
else:
|
||||
from plexpy import config
|
||||
from plexpy import database
|
||||
from plexpy import helpers
|
||||
from plexpy import libraries
|
||||
from plexpy import logger
|
||||
from plexpy import mobile_app
|
||||
from plexpy import notification_handler
|
||||
from plexpy import notifiers
|
||||
from plexpy import newsletter_handler
|
||||
from plexpy import newsletters
|
||||
from plexpy import users
|
||||
import jellypy
|
||||
from jellypy import common
|
||||
from jellypy import config
|
||||
from jellypy import database
|
||||
from jellypy import helpers
|
||||
from jellypy import libraries
|
||||
from jellypy import logger
|
||||
from jellypy import mobile_app
|
||||
from jellypy import notification_handler
|
||||
from jellypy import notifiers
|
||||
from jellypy import newsletter_handler
|
||||
from jellypy import newsletters
|
||||
from jellypy import users
|
||||
from jellypy.password import check_hash
|
||||
|
||||
|
||||
class API2(object):
|
||||
@@ -87,7 +69,8 @@ class API2(object):
|
||||
if md is True:
|
||||
docs[f] = inspect.getdoc(getattr(self, f)) if inspect.getdoc(getattr(self, f)) else None
|
||||
else:
|
||||
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(getattr(self, f)) else None
|
||||
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(
|
||||
getattr(self, f)) else None
|
||||
return docs
|
||||
|
||||
def docs_md(self):
|
||||
@@ -103,15 +86,15 @@ class API2(object):
|
||||
def _api_validate(self, *args, **kwargs):
|
||||
""" Sets class vars and remove unneeded parameters. """
|
||||
|
||||
if not plexpy.CONFIG.API_ENABLED:
|
||||
if not jellypy.CONFIG.API_ENABLED:
|
||||
self._api_msg = 'API not enabled'
|
||||
self._api_response_code = 404
|
||||
|
||||
elif not plexpy.CONFIG.API_KEY:
|
||||
elif not jellypy.CONFIG.API_KEY:
|
||||
self._api_msg = 'API key not generated'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif len(plexpy.CONFIG.API_KEY) != 32:
|
||||
elif len(jellypy.CONFIG.API_KEY) != 32:
|
||||
self._api_msg = 'API key not generated correctly'
|
||||
self._api_response_code = 401
|
||||
|
||||
@@ -124,7 +107,8 @@ class API2(object):
|
||||
self._api_response_code = 400
|
||||
|
||||
elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods:
|
||||
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods)))
|
||||
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (
|
||||
kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods)))
|
||||
self._api_response_code = 400
|
||||
|
||||
self._api_callback = kwargs.pop('callback', None)
|
||||
@@ -138,8 +122,8 @@ class API2(object):
|
||||
if 'app' in kwargs and helpers.bool_true(kwargs.pop('app')):
|
||||
self._api_app = True
|
||||
|
||||
if plexpy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
if not self._api_app and self._api_apikey == plexpy.CONFIG.API_KEY:
|
||||
if jellypy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
if not self._api_app and self._api_apikey == jellypy.CONFIG.API_KEY:
|
||||
self._api_authenticated = True
|
||||
|
||||
elif self._api_app and self._api_apikey == mobile_app.get_temp_device_token() and \
|
||||
@@ -199,7 +183,7 @@ class API2(object):
|
||||
]
|
||||
```
|
||||
"""
|
||||
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)
|
||||
logfile = os.path.join(jellypy.CONFIG.LOG_DIR, logger.FILENAME)
|
||||
templog = []
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
@@ -228,7 +212,6 @@ class API2(object):
|
||||
continue
|
||||
|
||||
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
||||
|
||||
d = {
|
||||
'time': temp_loglevel_and_time[0],
|
||||
'loglevel': loglvl,
|
||||
@@ -286,11 +269,11 @@ class API2(object):
|
||||
```
|
||||
"""
|
||||
|
||||
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
||||
interface_dir = os.path.join(jellypy.PROG_DIR, 'data/interfaces/')
|
||||
interface_list = [name for name in os.listdir(interface_dir) if
|
||||
os.path.isdir(os.path.join(interface_dir, name))]
|
||||
|
||||
conf = plexpy.CONFIG._config
|
||||
conf = jellypy.CONFIG._config
|
||||
config = {}
|
||||
|
||||
# Truthify the dict
|
||||
@@ -328,7 +311,7 @@ class API2(object):
|
||||
None
|
||||
```
|
||||
"""
|
||||
if not plexpy.CONFIG.API_SQL:
|
||||
if not jellypy.CONFIG.API_SQL:
|
||||
self._api_msg = 'SQL not enabled for the API.'
|
||||
return
|
||||
|
||||
@@ -338,12 +321,12 @@ class API2(object):
|
||||
|
||||
# allow the user to shoot them self
|
||||
# in the foot but not in the head..
|
||||
if not len(os.listdir(plexpy.CONFIG.BACKUP_DIR)):
|
||||
if not len(os.listdir(jellypy.CONFIG.BACKUP_DIR)):
|
||||
self.backup_db()
|
||||
else:
|
||||
# If the backup is less then 24 h old lets make a backup
|
||||
if not any(os.path.getctime(os.path.join(plexpy.CONFIG.BACKUP_DIR, file_)) > (time.time() - 86400)
|
||||
and file_.endswith('.db') for file_ in os.listdir(plexpy.CONFIG.BACKUP_DIR)):
|
||||
if not any(os.path.getctime(os.path.join(jellypy.CONFIG.BACKUP_DIR, file_)) > (time.time() - 86400)
|
||||
and file_.endswith('.db') for file_ in os.listdir(jellypy.CONFIG.BACKUP_DIR)):
|
||||
self.backup_db()
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
@@ -359,7 +342,7 @@ class API2(object):
|
||||
return data
|
||||
|
||||
def backup_db(self):
|
||||
""" Create a manual backup of the `plexpy.db` file."""
|
||||
""" Create a manual backup of the `jellypy.db` file."""
|
||||
|
||||
data = database.make_backup()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
@@ -369,14 +352,14 @@ class API2(object):
|
||||
def restart(self, **kwargs):
|
||||
""" Restart Tautulli."""
|
||||
|
||||
plexpy.SIGNAL = 'restart'
|
||||
jellypy.SIGNAL = 'restart'
|
||||
self._api_msg = 'Restarting Tautulli'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
def update(self, **kwargs):
|
||||
""" Update Tautulli."""
|
||||
|
||||
plexpy.SIGNAL = 'update'
|
||||
jellypy.SIGNAL = 'update'
|
||||
self._api_msg = 'Updating Tautulli'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
@@ -394,7 +377,8 @@ class API2(object):
|
||||
|
||||
return data
|
||||
|
||||
def register_device(self, device_id='', device_name='', friendly_name='', onesignal_id=None, **kwargs):
|
||||
def register_device(self, device_id='', device_name='', friendly_name='', onesignal_id=None,
|
||||
min_version='', **kwargs):
|
||||
""" Registers the Tautulli Android App for notifications.
|
||||
|
||||
```
|
||||
@@ -405,11 +389,32 @@ class API2(object):
|
||||
Optional parameters:
|
||||
friendly_name (str): A friendly name to identify the mobile device
|
||||
onesignal_id (str): The OneSignal id for the mobile device
|
||||
min_version (str): The minimum Tautulli version supported by the mobile device, e.g. v2.5.6
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"pms_name": "Winterfell-Server",
|
||||
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
|
||||
{"pms_identifier": "08u2phnlkdshf890bhdlksghnljsahgleikjfg9t",
|
||||
"pms_ip": "10.10.10.1",
|
||||
"pms_is_remote": 0,
|
||||
"pms_name": "Winterfell-Server",
|
||||
"pms_platform": "Windows",
|
||||
"pms_plexpass": 1,
|
||||
"pms_port": 32400,
|
||||
"pms_ssl": 0,
|
||||
"pms_url": "http://10.10.10.1:32400",
|
||||
"pms_url_manual": 0,
|
||||
"pms_version": "1.20.0.3133-fede5bdc7"
|
||||
"server_id": "2ce060c87958445d8399a7a0c5663755",
|
||||
"tautulli_install_type": "git",
|
||||
"tautulli_branch": "master",
|
||||
"tautulli_commit": "14b98a32e085d969f010f0249c3d2f660db50880",
|
||||
"tautulli_platform": "Windows",
|
||||
"tautulli_platform_device_name": "Winterfell-PC",
|
||||
"tautulli_platform_linux_distro": "",
|
||||
"tautulli_platform_release": "10",
|
||||
"tautulli_platform_version": "10.0.18362",
|
||||
"tautulli_python_version": "3.8.3"
|
||||
"tautulli_version": "v2.5.6",
|
||||
}
|
||||
```
|
||||
"""
|
||||
@@ -423,6 +428,12 @@ class API2(object):
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
elif min_version and helpers.version_to_tuple(min_version) > helpers.version_to_tuple(common.RELEASE):
|
||||
self._api_msg = 'Device registration failed: Tautulli version {} ' \
|
||||
'does not meet the minimum requirement of {}.'.format(common.RELEASE, min_version)
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
## TODO: Temporary for backwards compatibility, assume device_id is onesignal_id
|
||||
if device_id and onesignal_id is None:
|
||||
onesignal_id = device_id
|
||||
@@ -437,12 +448,16 @@ class API2(object):
|
||||
self._api_msg = 'Device registration successful.'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
mobile_app.set_temp_device_token(None)
|
||||
mobile_app.set_temp_device_token(True)
|
||||
|
||||
data = {
|
||||
"pms_name": plexpy.CONFIG.PMS_NAME,
|
||||
"server_id": plexpy.CONFIG.PMS_UUID
|
||||
}
|
||||
# TODO: Jellyfin
|
||||
# plex_server = plextv.get_server_resources(return_info=True)
|
||||
tautulli = jellypy.get_tautulli_info()
|
||||
|
||||
data = {"server_id": jellypy.CONFIG.PMS_UUID}
|
||||
# TODO: Jellyfin
|
||||
# data.update(plex_server)
|
||||
data.update(tautulli)
|
||||
|
||||
return data
|
||||
|
||||
@@ -545,8 +560,7 @@ class API2(object):
|
||||
def _api_make_md(self):
|
||||
""" Tries to make a API.md to simplify the api docs. """
|
||||
|
||||
head = '''# API Reference\n
|
||||
## General structure
|
||||
head = '''## General structure
|
||||
The API endpoint is
|
||||
```
|
||||
http://IP_ADDRESS:PORT + [/HTTP_ROOT] + /api/v2?apikey=$apikey&cmd=$command
|
||||
@@ -613,32 +627,32 @@ General optional parameters:
|
||||
"""
|
||||
data = None
|
||||
apikey = hashlib.sha224(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[0:32]
|
||||
if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD:
|
||||
if jellypy.CONFIG.HTTP_USERNAME and jellypy.CONFIG.HTTP_PASSWORD:
|
||||
authenticated = False
|
||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
||||
if jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == jellypy.CONFIG.HTTP_USERNAME and check_hash(password, jellypy.CONFIG.HTTP_PASSWORD):
|
||||
authenticated = True
|
||||
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||
elif not jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == jellypy.CONFIG.HTTP_USERNAME and password == jellypy.CONFIG.HTTP_PASSWORD:
|
||||
authenticated = True
|
||||
|
||||
if authenticated:
|
||||
if plexpy.CONFIG.API_KEY:
|
||||
data = plexpy.CONFIG.API_KEY
|
||||
if jellypy.CONFIG.API_KEY:
|
||||
data = jellypy.CONFIG.API_KEY
|
||||
else:
|
||||
data = apikey
|
||||
plexpy.CONFIG.API_KEY = apikey
|
||||
plexpy.CONFIG.write()
|
||||
jellypy.CONFIG.API_KEY = apikey
|
||||
jellypy.CONFIG.write()
|
||||
else:
|
||||
self._api_msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
||||
else:
|
||||
if plexpy.CONFIG.API_KEY:
|
||||
data = plexpy.CONFIG.API_KEY
|
||||
if jellypy.CONFIG.API_KEY:
|
||||
data = jellypy.CONFIG.API_KEY
|
||||
else:
|
||||
# Make a apikey if the doesn't exist
|
||||
data = apikey
|
||||
plexpy.CONFIG.API_KEY = apikey
|
||||
plexpy.CONFIG.write()
|
||||
jellypy.CONFIG.API_KEY = apikey
|
||||
jellypy.CONFIG.write()
|
||||
|
||||
return data
|
||||
|
||||
@@ -745,7 +759,8 @@ General optional parameters:
|
||||
|
||||
result = call(**self._api_kwargs)
|
||||
except Exception as e:
|
||||
logger.api_error('Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
|
||||
logger.api_error(
|
||||
'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
|
||||
self._api_response_code = 500
|
||||
if self._api_debug:
|
||||
cherrypy.request.show_tracebacks = True
|
||||
@@ -13,6 +13,7 @@ from OpenSSL import crypto
|
||||
TYPE_RSA = crypto.TYPE_RSA
|
||||
TYPE_DSA = crypto.TYPE_DSA
|
||||
|
||||
|
||||
def createKeyPair(type, bits):
|
||||
"""
|
||||
Create a public/private key pair.
|
||||
@@ -24,6 +25,7 @@ def createKeyPair(type, bits):
|
||||
pkey.generate_key(type, bits)
|
||||
return pkey
|
||||
|
||||
|
||||
def createCertRequest(pkey, digest="sha256", **name):
|
||||
"""
|
||||
Create a certificate request.
|
||||
@@ -50,6 +52,7 @@ def createCertRequest(pkey, digest="sha256", **name):
|
||||
req.sign(pkey, digest)
|
||||
return req
|
||||
|
||||
|
||||
def createCertificate(req, issuerCertKey, serial, validityPeriod, digest="sha256"):
|
||||
"""
|
||||
Generate a certificate given a certificate request.
|
||||
@@ -76,6 +79,7 @@ def createCertificate(req, issuerCertKey, serial, validityPeriod, digest="sha256
|
||||
cert.sign(issuerKey, digest)
|
||||
return cert
|
||||
|
||||
|
||||
def createSelfSignedCertificate(issuerName, issuerKey, serial, notBefore, notAfter, altNames, digest="sha256"):
|
||||
"""
|
||||
Generate a certificate given a certificate request.
|
||||
902
jellypy/common.py
Normal file
@@ -0,0 +1,902 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of Tautulli.
|
||||
#
|
||||
# Tautulli is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Tautulli is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import platform
|
||||
from collections import OrderedDict
|
||||
|
||||
import distro
|
||||
|
||||
from jellypy import version
|
||||
|
||||
# Identify Our Application
|
||||
PRODUCT = 'JellPy'
|
||||
PLATFORM = platform.system()
|
||||
PLATFORM_RELEASE = platform.release()
|
||||
PLATFORM_VERSION = platform.version()
|
||||
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
|
||||
PLATFORM_DEVICE_NAME = platform.node()
|
||||
PYTHON_VERSION = platform.python_version()
|
||||
BRANCH = version.PLEXPY_BRANCH
|
||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||
|
||||
USER_AGENT = '{}/{} ({} {})'.format(PRODUCT, RELEASE, PLATFORM, PLATFORM_RELEASE)
|
||||
|
||||
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
||||
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
||||
DEFAULT_ART = "interfaces/default/images/art.png"
|
||||
DEFAULT_LIVE_TV_POSTER_THUMB = "interfaces/default/images/poster-live.png"
|
||||
DEFAULT_LIVE_TV_ART = "interfaces/default/images/art-live.png"
|
||||
DEFAULT_LIVE_TV_ART_FULL = "interfaces/default/images/art-live-full.png"
|
||||
DEFAULT_LIVE_TV_THUMB = "interfaces/default/images/libraries/live.png"
|
||||
|
||||
ONLINE_POSTER_THUMB = "https://tautulli.com/images/poster.png"
|
||||
ONLINE_COVER_THUMB = "https://tautulli.com/images/cover.png"
|
||||
ONLINE_ART = "https://tautulli.com/images/art.png"
|
||||
|
||||
LIVE_TV_SECTION_ID = 999999 # Fake section_id for Live TV library
|
||||
LIVE_TV_SECTION_NAME = "Live TV" # Fake section_name for Live TV library
|
||||
|
||||
DEFAULT_IMAGES = {
|
||||
'poster': DEFAULT_POSTER_THUMB,
|
||||
'cover': DEFAULT_COVER_THUMB,
|
||||
'art': DEFAULT_ART,
|
||||
'poster-live': DEFAULT_LIVE_TV_POSTER_THUMB,
|
||||
'art-live': DEFAULT_LIVE_TV_ART,
|
||||
'art-live-full': DEFAULT_LIVE_TV_ART_FULL
|
||||
}
|
||||
|
||||
MEDIA_TYPE_HEADERS = {
|
||||
'movie': 'Movies',
|
||||
'show': 'TV Shows',
|
||||
'season': 'Seasons',
|
||||
'episode': 'Episodes',
|
||||
'artist': 'Artists',
|
||||
'album': 'Albums',
|
||||
'track': 'Tracks',
|
||||
'video': 'Videos',
|
||||
'audio': 'Tracks',
|
||||
'photo': 'Photos'
|
||||
}
|
||||
|
||||
PLATFORM_NAME_OVERRIDES = {
|
||||
'Konvergo': 'Plex Media Player',
|
||||
'Mystery 3': 'Playstation 3',
|
||||
'Mystery 4': 'Playstation 4',
|
||||
'Mystery 5': 'Xbox 360',
|
||||
'WebMAF': 'Playstation 4',
|
||||
'windows': 'Windows',
|
||||
'osx': 'macOS'
|
||||
}
|
||||
|
||||
PMS_PLATFORM_NAME_OVERRIDES = {
|
||||
'MacOSX': 'Mac'
|
||||
}
|
||||
|
||||
PLATFORM_NAMES = {
|
||||
'android': 'android',
|
||||
'apple tv': 'atv',
|
||||
'chrome': 'chrome',
|
||||
'chromecast': 'chromecast',
|
||||
'dlna': 'dlna',
|
||||
'firefox': 'firefox',
|
||||
'internet explorer': 'ie',
|
||||
'ios': 'ios',
|
||||
'ipad': 'ios',
|
||||
'iphone': 'ios',
|
||||
'kodi': 'kodi',
|
||||
'linux': 'linux',
|
||||
'nexus': 'android',
|
||||
'macos': 'macos',
|
||||
'microsoft edge': 'msedge',
|
||||
'netcast': 'lg',
|
||||
'opera': 'opera',
|
||||
'osx': 'macos',
|
||||
'playstation': 'playstation',
|
||||
'plex home theater': 'plex',
|
||||
'plex media player': 'plex',
|
||||
'plexamp': 'plexamp',
|
||||
'plextogether': 'synclounge',
|
||||
'roku': 'roku',
|
||||
'safari': 'safari',
|
||||
'samsung': 'samsung',
|
||||
'synclounge': 'synclounge',
|
||||
'tivo': 'tivo',
|
||||
'tizen': 'samsung',
|
||||
'tvos': 'atv',
|
||||
'vizio': 'opera',
|
||||
'webos': 'lg',
|
||||
'wiiu': 'wiiu',
|
||||
'windows': 'windows',
|
||||
'windows phone': 'wp',
|
||||
'xbmc': 'xbmc',
|
||||
'xbox': 'xbox'
|
||||
}
|
||||
PLATFORM_NAMES = OrderedDict(sorted(list(PLATFORM_NAMES.items()), key=lambda k: k[0], reverse=True))
|
||||
|
||||
MEDIA_FLAGS_AUDIO = {
|
||||
'ac.?3': 'dolby_digital',
|
||||
'truehd': 'dolby_truehd',
|
||||
'(dca|dta)': 'dts',
|
||||
'dts(hd_|-hd|-)?ma': 'dca-ma',
|
||||
'vorbis': 'ogg'
|
||||
}
|
||||
MEDIA_FLAGS_VIDEO = {
|
||||
'avc1': 'h264',
|
||||
'wmv(1|2)': 'wmv',
|
||||
'wmv3': 'wmvhd'
|
||||
}
|
||||
|
||||
AUDIO_CODEC_OVERRIDES = {
|
||||
'truehd': 'TrueHD'
|
||||
}
|
||||
|
||||
VIDEO_RESOLUTION_OVERRIDES = {
|
||||
'sd': 'SD',
|
||||
'4k': '4k'
|
||||
}
|
||||
|
||||
AUDIO_CHANNELS = {
|
||||
'1': 'Mono',
|
||||
'2': 'Stereo',
|
||||
'3': '2.1',
|
||||
'4': '3.1',
|
||||
'6': '5.1',
|
||||
'7': '6.1',
|
||||
'8': '7.1'
|
||||
}
|
||||
|
||||
VIDEO_QUALITY_PROFILES = {
|
||||
20000: '20 Mbps 1080p',
|
||||
12000: '12 Mbps 1080p',
|
||||
10000: '10 Mbps 1080p',
|
||||
8000: '8 Mbps 1080p',
|
||||
4000: '4 Mbps 720p',
|
||||
3000: '3 Mbps 720p',
|
||||
2000: '2 Mbps 720p',
|
||||
1500: '1.5 Mbps 480p',
|
||||
720: '0.7 Mbps 328p',
|
||||
320: '0.3 Mbps 240p',
|
||||
208: '0.2 Mbps 160p',
|
||||
96: '0.096 Mbps',
|
||||
64: '0.064 Mbps'
|
||||
}
|
||||
VIDEO_QUALITY_PROFILES = OrderedDict(sorted(list(VIDEO_QUALITY_PROFILES.items()), key=lambda k: k[0], reverse=True))
|
||||
|
||||
AUDIO_QUALITY_PROFILES = {
|
||||
512: '512 kbps',
|
||||
320: '320 kbps',
|
||||
256: '256 kbps',
|
||||
192: '192 kbps',
|
||||
128: '128 kbps',
|
||||
96: '96 kbps'
|
||||
}
|
||||
AUDIO_QUALITY_PROFILES = OrderedDict(sorted(list(AUDIO_QUALITY_PROFILES.items()), key=lambda k: k[0], reverse=True))
|
||||
|
||||
HW_DECODERS = [
|
||||
'dxva2',
|
||||
'videotoolbox',
|
||||
'mediacodecndk',
|
||||
'vaapi',
|
||||
'nvdec'
|
||||
]
|
||||
HW_ENCODERS = [
|
||||
'qsv',
|
||||
'mf',
|
||||
'videotoolbox',
|
||||
'mediacodecndk',
|
||||
'vaapi',
|
||||
'nvenc',
|
||||
'x264'
|
||||
]
|
||||
|
||||
EXTRA_TYPES = {
|
||||
'1': 'Trailer',
|
||||
'2': 'Deleted Scene',
|
||||
'3': 'Interview',
|
||||
'5': 'Behind the Scenes',
|
||||
'6': 'Scene',
|
||||
'10': 'Featurette',
|
||||
'11': 'Short'
|
||||
}
|
||||
|
||||
SCHEDULER_LIST = [
|
||||
('Check GitHub for updates', 'websocket'),
|
||||
('Check for server response', 'websocket'),
|
||||
('Check for active sessions', 'websocket'),
|
||||
('Check for recently added items', 'websocket'),
|
||||
('Check for server remote access', 'websocket'),
|
||||
('Check for Plex updates', 'scheduled'),
|
||||
('Refresh users list', 'scheduled'),
|
||||
('Refresh libraries list', 'scheduled'),
|
||||
('Refresh Plex server URLs', 'scheduled'),
|
||||
('Optimize Tautulli database', 'scheduled'),
|
||||
('Backup Tautulli database', 'scheduled'),
|
||||
('Backup Tautulli config', 'scheduled')
|
||||
]
|
||||
SCHEDULER_LIST = OrderedDict(SCHEDULER_LIST)
|
||||
|
||||
DATE_TIME_FORMATS = [
|
||||
{
|
||||
'category': 'Year',
|
||||
'parameters': [
|
||||
{'value': 'YYYY', 'description': 'Numeric, four digits', 'example': '1999, 2003'},
|
||||
{'value': 'YY', 'description': 'Numeric, two digits', 'example': '99, 03'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Month',
|
||||
'parameters': [
|
||||
{'value': 'MMMM', 'description': 'Textual, full', 'example': 'January-December'},
|
||||
{'value': 'MMM', 'description': 'Textual, three letters', 'example': 'Jan-Dec'},
|
||||
{'value': 'MM', 'description': 'Numeric, with leading zeros', 'example': '01-12'},
|
||||
{'value': 'M', 'description': 'Numeric, without leading zeros', 'example': '1-12'},
|
||||
{'value': 'Mo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 12th'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Day of the Year',
|
||||
'parameters': [
|
||||
{'value': 'DDDD', 'description': 'Numeric, with leading zeros', 'example': '001-365'},
|
||||
{'value': 'DDD', 'description': 'Numeric, without leading zeros', 'example': '1-365'},
|
||||
{'value': 'DDDo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd, ... 365th'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Day of the Month',
|
||||
'parameters': [
|
||||
{'value': 'DD', 'description': 'Numeric, with leading zeros', 'example': '01-31'},
|
||||
{'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '1-31'},
|
||||
{'value': 'Do', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 31st'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Day of the Week',
|
||||
'parameters': [
|
||||
{'value': 'dddd', 'description': 'Textual, full', 'example': 'Sunday-Saturday'},
|
||||
{'value': 'ddd', 'description': 'Textual, three letters', 'example': 'Sun-Sat'},
|
||||
{'value': 'dd', 'description': 'Textual, two letters', 'example': 'Su-Sa'},
|
||||
{'value': 'd', 'description': 'Numeric', 'example': '0-6'},
|
||||
{'value': 'do', 'description': 'Numeric, with suffix', 'example': '0th, 1st ... 6th'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Hour',
|
||||
'parameters': [
|
||||
{'value': 'HH', 'description': '24-hour, with leading zeros', 'example': '00-23'},
|
||||
{'value': 'H', 'description': '24-hour, without leading zeros', 'example': '0-23'},
|
||||
{'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '01-12'},
|
||||
{'value': 'h', 'description': '12-hour, without leading zeros', 'example': '1-12'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Minute',
|
||||
'parameters': [
|
||||
{'value': 'mm', 'description': 'Numeric, with leading zeros', 'example': '00-59'},
|
||||
{'value': 'm', 'description': 'Numeric, without leading zeros', 'example': '0-59'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Second',
|
||||
'parameters': [
|
||||
{'value': 'ss', 'description': 'Numeric, with leading zeros', 'example': '00-59'},
|
||||
{'value': 's', 'description': 'Numeric, without leading zeros', 'example': '0-59'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'AM / PM',
|
||||
'parameters': [
|
||||
{'value': 'A', 'description': 'AM/PM uppercase', 'example': 'AM, PM'},
|
||||
{'value': 'a', 'description': 'am/pm lowercase', 'example': 'am, pm'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Timezone',
|
||||
'parameters': [
|
||||
{'value': 'ZZ', 'description': 'UTC offset', 'example': '+0100, -0700'},
|
||||
{'value': 'Z', 'description': 'UTC offset', 'example': '+01:00, -07:00'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Timestamp',
|
||||
'parameters': [
|
||||
{'value': 'X', 'description': 'Unix timestamp', 'example': 'E.g. 1456887825'},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
NOTIFICATION_PARAMETERS = [
|
||||
{
|
||||
'category': 'Global',
|
||||
'parameters': [
|
||||
{'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version',
|
||||
'description': 'The current version of Tautulli.'},
|
||||
{'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote',
|
||||
'description': 'The current git remote of Tautulli.'},
|
||||
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch',
|
||||
'description': 'The current git branch of Tautulli.'},
|
||||
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit',
|
||||
'description': 'The current git commit hash of Tautulli.'},
|
||||
{'name': 'Server Name', 'type': 'str', 'value': 'server_name',
|
||||
'description': 'The name of your Plex Server.'},
|
||||
{'name': 'Server IP', 'type': 'str', 'value': 'server_ip',
|
||||
'description': 'The connection IP address for your Plex Server.'},
|
||||
{'name': 'Server Port', 'type': 'int', 'value': 'server_port',
|
||||
'description': 'The connection port for your Plex Server.'},
|
||||
{'name': 'Server URL', 'type': 'str', 'value': 'server_url',
|
||||
'description': 'The connection URL for your Plex Server.'},
|
||||
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform',
|
||||
'description': 'The platform of your Plex Server.'},
|
||||
{'name': 'Server Version', 'type': 'str', 'value': 'server_version',
|
||||
'description': 'The current version of your Plex Server.'},
|
||||
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id',
|
||||
'description': 'The unique identifier for your Plex Server.'},
|
||||
{'name': 'Action', 'type': 'str', 'value': 'action',
|
||||
'description': 'The action that triggered the notification.'},
|
||||
{'name': 'Current Year', 'type': 'int', 'value': 'current_year',
|
||||
'description': 'The year when the notification is triggered.'},
|
||||
{'name': 'Current Month', 'type': 'int', 'value': 'current_month',
|
||||
'description': 'The month when the notification is triggered.', 'example': '1 to 12'},
|
||||
{'name': 'Current Day', 'type': 'int', 'value': 'current_day',
|
||||
'description': 'The day when the notification is triggered.', 'example': '1 to 31'},
|
||||
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour',
|
||||
'description': 'The hour when the notification is triggered.', 'example': '0 to 23'},
|
||||
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute',
|
||||
'description': 'The minute when the notification is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Second', 'type': 'int', 'value': 'current_second',
|
||||
'description': 'The second when the notification is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday',
|
||||
'description': 'The ISO weekday when the notification is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
|
||||
{'name': 'Current Week', 'type': 'int', 'value': 'current_week',
|
||||
'description': 'The ISO week number when the notification is triggered.', 'example': '1 to 52'},
|
||||
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp',
|
||||
'description': 'The date (in date format) when the notification is triggered.'},
|
||||
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp',
|
||||
'description': 'The time (in time format) when the notification is triggered.'},
|
||||
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime',
|
||||
'description': 'The unix timestamp when the notification is triggered.'},
|
||||
{'name': 'UTC Time', 'type': 'int', 'value': 'utctime',
|
||||
'description': 'The UTC timestamp in ISO format when the notification is triggered.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Stream Details',
|
||||
'parameters': [
|
||||
{'name': 'Streams', 'type': 'int', 'value': 'streams',
|
||||
'description': 'The total number of concurrent streams.'},
|
||||
{'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays',
|
||||
'description': 'The total number of concurrent direct plays.'},
|
||||
{'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams',
|
||||
'description': 'The total number of concurrent direct streams.'},
|
||||
{'name': 'Transcodes', 'type': 'int', 'value': 'transcodes',
|
||||
'description': 'The total number of concurrent transcodes.'},
|
||||
{'name': 'Total Bandwidth', 'type': 'int', 'value': 'total_bandwidth',
|
||||
'description': 'The total Plex Streaming Brain reserved bandwidth (in kbps).',
|
||||
'help_text': 'not the used bandwidth'},
|
||||
{'name': 'LAN Bandwidth', 'type': 'int', 'value': 'lan_bandwidth',
|
||||
'description': 'The total Plex Streaming Brain reserved LAN bandwidth (in kbps).',
|
||||
'help_text': 'not the used bandwidth'},
|
||||
{'name': 'WAN Bandwidth', 'type': 'int', 'value': 'wan_bandwidth',
|
||||
'description': 'The total Plex Streaming Brain reserved WAN bandwidth (in kbps).',
|
||||
'help_text': 'not the used bandwidth'},
|
||||
{'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': 'User Thumb', 'type': 'str', 'value': 'user_thumb',
|
||||
'description': 'The profile picture URL 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.'},
|
||||
{'name': 'Player', 'type': 'str', 'value': 'player',
|
||||
'description': 'The name of the player being used for playback.'},
|
||||
{'name': 'Initial Stream', 'type': 'int', 'value': 'initial_stream',
|
||||
'description': 'If the stream is the initial stream of a continuous streaming session.',
|
||||
'example': '0 or 1'},
|
||||
{'name': 'IP Address', 'type': 'str', 'value': 'ip_address',
|
||||
'description': 'The IP address of the device being used for playback.'},
|
||||
{'name': 'Stream Duration', 'type': 'int', 'value': 'stream_duration',
|
||||
'description': 'The duration (in minutes) for the stream.'},
|
||||
{'name': 'Stream Time', 'type': 'str', 'value': 'stream_time',
|
||||
'description': 'The duration (in time format) of the stream.'},
|
||||
{'name': 'Remaining Duration', 'type': 'int', 'value': 'remaining_duration',
|
||||
'description': 'The remaining duration (in minutes) of the stream.'},
|
||||
{'name': 'Remaining Time', 'type': 'str', 'value': 'remaining_time',
|
||||
'description': 'The remaining duration (in time format) of the stream.'},
|
||||
{'name': 'Progress Duration', 'type': 'int', 'value': 'progress_duration',
|
||||
'description': 'The last reported offset (in minutes) of the stream.'},
|
||||
{'name': 'Progress Time', 'type': 'str', 'value': 'progress_time',
|
||||
'description': 'The last reported offset (in time format) of the stream.'},
|
||||
{'name': 'Progress Percent', 'type': 'int', 'value': 'progress_percent',
|
||||
'description': 'The last reported progress percent of the stream.'},
|
||||
{'name': 'Transcode Decision', 'type': 'str', 'value': 'transcode_decision',
|
||||
'description': 'The transcode decision of the stream.'},
|
||||
{'name': 'Container Decision', 'type': 'str', 'value': 'container_decision',
|
||||
'description': 'The container transcode decision of the stream.'},
|
||||
{'name': 'Video Decision', 'type': 'str', 'value': 'video_decision',
|
||||
'description': 'The video transcode decision of the stream.'},
|
||||
{'name': 'Audio Decision', 'type': 'str', 'value': 'audio_decision',
|
||||
'description': 'The audio transcode decision of the stream.'},
|
||||
{'name': 'Subtitle Decision', 'type': 'str', 'value': 'subtitle_decision',
|
||||
'description': 'The subtitle transcode decision of the stream.'},
|
||||
{'name': 'Quality Profile', 'type': 'str', 'value': 'quality_profile',
|
||||
'description': 'The Plex quality profile of the stream.', 'example': 'e.g. Original, 4 Mbps 720p, etc.'},
|
||||
{'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version',
|
||||
'description': 'If the stream is an optimized version.', 'example': '0 or 1'},
|
||||
{'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile',
|
||||
'description': 'The optimized version profile of the stream.'},
|
||||
{'name': 'Synced Version', 'type': 'int', 'value': 'synced_version',
|
||||
'description': 'If the stream is an synced version.', 'example': '0 or 1'},
|
||||
{'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.',
|
||||
'example': '0 or 1'},
|
||||
{'name': 'Channel Call Sign', 'type': 'str', 'value': 'channel_call_sign',
|
||||
'description': 'The Live TV channel call sign.'},
|
||||
{'name': 'Channel Identifier', 'type': 'str', 'value': 'channel_identifier',
|
||||
'description': 'The Live TV channel number.'},
|
||||
{'name': 'Channel Thumb', 'type': 'str', 'value': 'channel_thumb',
|
||||
'description': 'The URL for the Live TV channel logo.'},
|
||||
{'name': 'Secure', 'type': 'int', 'value': 'secure',
|
||||
'description': 'If the stream is using a secure connection.', 'example': '0 or 1'},
|
||||
{'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.',
|
||||
'example': '0 or 1'},
|
||||
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.',
|
||||
'example': '0 or 1'},
|
||||
{'name': 'Stream Location', 'type': 'str', 'value': 'stream_location',
|
||||
'description': 'The network location of the stream.', 'example': 'lan or wan'},
|
||||
{'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth',
|
||||
'description': 'The Plex Streaming Brain reserved bandwidth (in kbps) of the stream.',
|
||||
'help_text': 'not the used bandwidth'},
|
||||
{'name': 'Stream Container', 'type': 'str', 'value': 'stream_container',
|
||||
'description': 'The media container of the stream.'},
|
||||
{'name': 'Stream Bitrate', 'type': 'int', 'value': 'stream_bitrate',
|
||||
'description': 'The bitrate (in kbps) of the stream.'},
|
||||
{'name': 'Stream Aspect Ratio', 'type': 'float', 'value': 'stream_aspect_ratio',
|
||||
'description': 'The aspect ratio of the stream.'},
|
||||
{'name': 'Stream Video Codec', 'type': 'str', 'value': 'stream_video_codec',
|
||||
'description': 'The video codec of the stream.'},
|
||||
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level',
|
||||
'description': 'The video codec level of the stream.'},
|
||||
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate',
|
||||
'description': 'The video bitrate (in kbps) of the stream.'},
|
||||
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth',
|
||||
'description': 'The video bit depth of the stream.'},
|
||||
{'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling',
|
||||
'description': 'The video chroma subsampling of the stream.'},
|
||||
{'name': 'Stream Video Color Primaries', 'type': 'str', 'value': 'stream_video_color_primaries',
|
||||
'description': 'The video color primaries of the stream.'},
|
||||
{'name': 'Stream Video Color Range', 'type': 'str', 'value': 'stream_video_color_range',
|
||||
'description': 'The video color range of the stream.'},
|
||||
{'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space',
|
||||
'description': 'The video color space of the stream.'},
|
||||
{'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc',
|
||||
'description': 'The video transfer function of the stream.'},
|
||||
{'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range',
|
||||
'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'},
|
||||
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate',
|
||||
'description': 'The video framerate of the stream.'},
|
||||
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution',
|
||||
'description': 'The video resolution of the stream with scan type.'},
|
||||
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames',
|
||||
'description': 'The video reference frames of the stream.'},
|
||||
{'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution',
|
||||
'description': 'The video resolution of the stream.'},
|
||||
{'name': 'Stream Video Scan Type', 'type': 'str', 'value': 'stream_video_scan_type',
|
||||
'description': 'The video scan type of the stream.'},
|
||||
{'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height',
|
||||
'description': 'The video height of the stream.'},
|
||||
{'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width',
|
||||
'description': 'The video width of the stream.'},
|
||||
{'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language',
|
||||
'description': 'The video language of the stream.'},
|
||||
{'name': 'Stream Video Language Code', 'type': 'str', 'value': 'stream_video_language_code',
|
||||
'description': 'The video language code of the stream.'},
|
||||
{'name': 'Stream Audio Bitrate', 'type': 'int', 'value': 'stream_audio_bitrate',
|
||||
'description': 'The audio bitrate of the stream.'},
|
||||
{'name': 'Stream Audio Bitrate Mode', 'type': 'str', 'value': 'stream_audio_bitrate_mode',
|
||||
'description': 'The audio bitrate mode of the stream.', 'example': 'cbr or vbr'},
|
||||
{'name': 'Stream Audio Codec', 'type': 'str', 'value': 'stream_audio_codec',
|
||||
'description': 'The audio codec of the stream.'},
|
||||
{'name': 'Stream Audio Channels', 'type': 'float', 'value': 'stream_audio_channels',
|
||||
'description': 'The audio channels of the stream.'},
|
||||
{'name': 'Stream Audio Channel Layout', 'type': 'str', 'value': 'stream_audio_channel_layout',
|
||||
'description': 'The audio channel layout of the stream.'},
|
||||
{'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate',
|
||||
'description': 'The audio sample rate (in Hz) of the stream.'},
|
||||
{'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language',
|
||||
'description': 'The audio language of the stream.'},
|
||||
{'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code',
|
||||
'description': 'The audio language code of the stream.'},
|
||||
{'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec',
|
||||
'description': 'The subtitle codec of the stream.'},
|
||||
{'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container',
|
||||
'description': 'The subtitle container of the stream.'},
|
||||
{'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format',
|
||||
'description': 'The subtitle format of the stream.'},
|
||||
{'name': 'Stream Subtitle Forced', 'type': 'int', 'value': 'stream_subtitle_forced',
|
||||
'description': 'If the subtitles are forced.', 'example': '0 or 1'},
|
||||
{'name': 'Stream Subtitle Language', 'type': 'str', 'value': 'stream_subtitle_language',
|
||||
'description': 'The subtitle language of the stream.'},
|
||||
{'name': 'Stream Subtitle Language Code', 'type': 'str', 'value': 'stream_subtitle_language_code',
|
||||
'description': 'The subtitle language code of the stream.'},
|
||||
{'name': 'Stream Subtitle Location', 'type': 'str', 'value': 'stream_subtitle_location',
|
||||
'description': 'The subtitle location of the stream.'},
|
||||
{'name': 'Transcode Container', 'type': 'str', 'value': 'transcode_container',
|
||||
'description': 'The media container of the transcoded stream.'},
|
||||
{'name': 'Transcode Video Codec', 'type': 'str', 'value': 'transcode_video_codec',
|
||||
'description': 'The video codec of the transcoded stream.'},
|
||||
{'name': 'Transcode Video Width', 'type': 'int', 'value': 'transcode_video_width',
|
||||
'description': 'The video width of the transcoded stream.'},
|
||||
{'name': 'Transcode Video Height', 'type': 'int', 'value': 'transcode_video_height',
|
||||
'description': 'The video height of the transcoded stream.'},
|
||||
{'name': 'Transcode Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec',
|
||||
'description': 'The audio codec of the transcoded stream.'},
|
||||
{'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels',
|
||||
'description': 'The audio channels of the transcoded stream.'},
|
||||
{'name': 'Transcode HW Requested', 'type': 'int', 'value': 'transcode_hw_requested',
|
||||
'description': 'If hardware decoding/encoding was requested.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Decoding', 'type': 'int', 'value': 'transcode_hw_decoding',
|
||||
'description': 'If hardware decoding is used.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Decoding Codec', 'type': 'str', 'value': 'transcode_hw_decode',
|
||||
'description': 'The hardware decoding codec.'},
|
||||
{'name': 'Transcode HW Decoding Title', 'type': 'str', 'value': 'transcode_hw_decode_title',
|
||||
'description': 'The hardware decoding codec title.'},
|
||||
{'name': 'Transcode HW Encoding', 'type': 'int', 'value': 'transcode_hw_encoding',
|
||||
'description': 'If hardware encoding is used.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Encoding Codec', 'type': 'str', 'value': 'transcode_hw_encode',
|
||||
'description': 'The hardware encoding codec.'},
|
||||
{'name': 'Transcode HW Encoding Title', 'type': 'str', 'value': 'transcode_hw_encode_title',
|
||||
'description': 'The hardware encoding codec title.'},
|
||||
{'name': 'Session Key', 'type': 'str', 'value': 'session_key',
|
||||
'description': 'The unique identifier for the session.'},
|
||||
{'name': 'Transcode Key', 'type': 'str', 'value': 'transcode_key',
|
||||
'description': 'The unique identifier for the transcode session.'},
|
||||
{'name': 'Session ID', 'type': 'str', 'value': 'session_id',
|
||||
'description': 'The unique identifier for the stream.'},
|
||||
{'name': 'User ID', 'type': 'int', 'value': 'user_id',
|
||||
'description': 'The unique identifier for the user.'},
|
||||
{'name': 'Machine ID', 'type': 'str', 'value': 'machine_id',
|
||||
'description': 'The unique identifier for the player.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Source Metadata Details',
|
||||
'parameters': [
|
||||
{'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.',
|
||||
'example': 'movie, show, season, episode, artist, album, track, clip'},
|
||||
{'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'},
|
||||
{'name': 'Library Name', 'type': 'str', 'value': 'library_name',
|
||||
'description': 'The library name of the item.'},
|
||||
{'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'},
|
||||
{'name': 'Episode Name', 'type': 'str', 'value': 'episode_name',
|
||||
'description': 'The title of the episode.'},
|
||||
{'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'},
|
||||
{'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'},
|
||||
{'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'},
|
||||
{'name': 'Track Artist', 'type': 'str', 'value': 'track_artist',
|
||||
'description': 'The name of the artist of the track.'},
|
||||
{'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.',
|
||||
'example': 'e.g. 1, or 1-3'},
|
||||
{'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00',
|
||||
'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'},
|
||||
{'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.',
|
||||
'example': 'e.g. 6, or 6-10'},
|
||||
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00',
|
||||
'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
|
||||
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.',
|
||||
'example': 'e.g. 4, or 4-10'},
|
||||
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00',
|
||||
'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
|
||||
{'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'},
|
||||
{'name': 'Episode Count', 'type': 'int', 'value': 'episode_count',
|
||||
'description': 'The number of episodes.'},
|
||||
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
|
||||
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
|
||||
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
|
||||
{'name': 'Release Date', 'type': 'str', 'value': 'release_date',
|
||||
'description': 'The release date (in date format) for the item.'},
|
||||
{'name': 'Air Date', 'type': 'str', 'value': 'air_date',
|
||||
'description': 'The air date (in date format) for the item.'},
|
||||
{'name': 'Added Date', 'type': 'str', 'value': 'added_date',
|
||||
'description': 'The date (in date format) the item was added to Plex.'},
|
||||
{'name': 'Updated Date', 'type': 'str', 'value': 'updated_date',
|
||||
'description': 'The date (in date format) the item was updated on Plex.'},
|
||||
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date',
|
||||
'description': 'The date (in date format) the item was last viewed on Plex.'},
|
||||
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
|
||||
{'name': 'Content Rating', 'type': 'str', 'value': 'content_rating',
|
||||
'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
||||
{'name': 'Directors', 'type': 'str', 'value': 'directors',
|
||||
'description': 'A list of directors for the item.'},
|
||||
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
|
||||
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
|
||||
{'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
|
||||
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
|
||||
{'name': 'Collections', 'type': 'str', 'value': 'collections',
|
||||
'description': 'A list of collections for the item.'},
|
||||
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
|
||||
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
|
||||
{'name': 'Rating', 'type': 'float', 'value': 'rating',
|
||||
'description': 'The rating (out of 10) for the item.'},
|
||||
{'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating',
|
||||
'description': 'The critic rating (%) for the item.',
|
||||
'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
|
||||
{'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating',
|
||||
'description': 'The audience rating for the item.',
|
||||
'help_text': 'Rating out of 10 for IMDB, percentage (%) for Rotten Tomatoes and TMDB.'},
|
||||
{'name': 'User Rating', 'type': 'float', 'value': 'user_rating',
|
||||
'description': 'The user (star) rating (out of 10) for the item.'},
|
||||
{'name': 'Duration', 'type': 'int', 'value': 'duration',
|
||||
'description': 'The duration (in minutes) for the item.'},
|
||||
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url',
|
||||
'description': 'A URL for the movie, TV show, or album poster.'},
|
||||
{'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.',
|
||||
'example': 'e.g. 5d7769a9594b2b001e6a6b7e'},
|
||||
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url',
|
||||
'description': 'The Plex URL to your server for the item.'},
|
||||
{'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.',
|
||||
'example': 'e.g. tt2488496'},
|
||||
{'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'},
|
||||
{'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the TV show.',
|
||||
'example': 'e.g. 121361'},
|
||||
{'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the TV show.'},
|
||||
{'name': 'TMDB ID', 'type': 'int', 'value': 'themoviedb_id',
|
||||
'description': 'The TMDb ID for the movie or TV show.', 'example': 'e.g. 15260'},
|
||||
{'name': 'TMDB URL', 'type': 'str', 'value': 'themoviedb_url',
|
||||
'description': 'The TMDb URL for the movie or TV show.'},
|
||||
{'name': 'TVmaze ID', 'type': 'int', 'value': 'tvmaze_id', 'description': 'The TVmaze ID for the TV show.',
|
||||
'example': 'e.g. 290'},
|
||||
{'name': 'TVmaze URL', 'type': 'str', 'value': 'tvmaze_url',
|
||||
'description': 'The TVmaze URL for the TV show.'},
|
||||
{'name': 'MusicBrainz ID', 'type': 'str', 'value': 'musicbrainz_id',
|
||||
'description': 'The MusicBrainz ID for the artist, album, or track.',
|
||||
'example': 'e.g. b670dfcf-9824-4309-a57e-03595aaba286'},
|
||||
{'name': 'MusicBrainz URL', 'type': 'str', 'value': 'musicbrainz_url',
|
||||
'description': 'The MusicBrainz URL for the artist, album, or track.'},
|
||||
{'name': 'Last.fm URL', 'type': 'str', 'value': 'lastfm_url',
|
||||
'description': 'The Last.fm URL for the album.', 'help_text': 'Music library agent must be Last.fm'},
|
||||
{'name': 'Trakt.tv URL', 'type': 'str', 'value': 'trakt_url',
|
||||
'description': 'The trakt.tv URL for the movie or TV show.'},
|
||||
{'name': 'Container', 'type': 'str', 'value': 'container',
|
||||
'description': 'The media container of the original media.'},
|
||||
{'name': 'Bitrate', 'type': 'int', 'value': 'bitrate', 'description': 'The bitrate of the original media.'},
|
||||
{'name': 'Aspect Ratio', 'type': 'float', 'value': 'aspect_ratio',
|
||||
'description': 'The aspect ratio of the original media.'},
|
||||
{'name': 'Video Codec', 'type': 'str', 'value': 'video_codec',
|
||||
'description': 'The video codec of the original media.'},
|
||||
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level',
|
||||
'description': 'The video codec level of the original media.'},
|
||||
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate',
|
||||
'description': 'The video bitrate of the original media.'},
|
||||
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth',
|
||||
'description': 'The video bit depth of the original media.'},
|
||||
{'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling',
|
||||
'description': 'The video chroma subsampling of the original media.'},
|
||||
{'name': 'Video Color Primaries', 'type': 'str', 'value': 'video_color_primaries',
|
||||
'description': 'The video color primaries of the original media.'},
|
||||
{'name': 'Video Color Range', 'type': 'str', 'value': 'video_color_range',
|
||||
'description': 'The video color range of the original media.'},
|
||||
{'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space',
|
||||
'description': 'The video color space of the original media.'},
|
||||
{'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc',
|
||||
'description': 'The video transfer function of the original media.'},
|
||||
{'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range',
|
||||
'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'},
|
||||
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate',
|
||||
'description': 'The video framerate of the original media.'},
|
||||
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution',
|
||||
'description': 'The video resolution of the original media with scan type.'},
|
||||
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames',
|
||||
'description': 'The video reference frames of the original media.'},
|
||||
{'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution',
|
||||
'description': 'The video resolution of the original media.'},
|
||||
{'name': 'Video Scan Type', 'type': 'str', 'value': 'video_scan_type',
|
||||
'description': 'The video scan type of the original media.'},
|
||||
{'name': 'Video Height', 'type': 'int', 'value': 'video_height',
|
||||
'description': 'The video height of the original media.'},
|
||||
{'name': 'Video Width', 'type': 'int', 'value': 'video_width',
|
||||
'description': 'The video width of the original media.'},
|
||||
{'name': 'Video Language', 'type': 'str', 'value': 'video_language',
|
||||
'description': 'The video language of the original media.'},
|
||||
{'name': 'Video Language Code', 'type': 'str', 'value': 'video_language_code',
|
||||
'description': 'The video language code of the original media.'},
|
||||
{'name': 'Audio Bitrate', 'type': 'int', 'value': 'audio_bitrate',
|
||||
'description': 'The audio bitrate of the original media.'},
|
||||
{'name': 'Audio Bitrate Mode', 'type': 'str', 'value': 'audio_bitrate_mode',
|
||||
'description': 'The audio bitrate mode of the original media.', 'example': 'cbr or vbr'},
|
||||
{'name': 'Audio Codec', 'type': 'str', 'value': 'audio_codec',
|
||||
'description': 'The audio codec of the original media.'},
|
||||
{'name': 'Audio Channels', 'type': 'float', 'value': 'audio_channels',
|
||||
'description': 'The audio channels of the original media.'},
|
||||
{'name': 'Audio Channel Layout', 'type': 'str', 'value': 'audio_channel_layout',
|
||||
'description': 'The audio channel layout of the original media.'},
|
||||
{'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate',
|
||||
'description': 'The audio sample rate (in Hz) of the original media.'},
|
||||
{'name': 'Audio Language', 'type': 'str', 'value': 'audio_language',
|
||||
'description': 'The audio language of the original media.'},
|
||||
{'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code',
|
||||
'description': 'The audio language code of the original media.'},
|
||||
{'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec',
|
||||
'description': 'The subtitle codec of the original media.'},
|
||||
{'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container',
|
||||
'description': 'The subtitle container of the original media.'},
|
||||
{'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format',
|
||||
'description': 'The subtitle format of the original media.'},
|
||||
{'name': 'Subtitle Forced', 'type': 'int', 'value': 'subtitle_forced',
|
||||
'description': 'If the subtitles are forced.', 'example': '0 or 1'},
|
||||
{'name': 'Subtitle Location', 'type': 'str', 'value': 'subtitle_location',
|
||||
'description': 'The subtitle location of the original media.'},
|
||||
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language',
|
||||
'description': 'The subtitle language of the original media.'},
|
||||
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code',
|
||||
'description': 'The subtitle language code of the original media.'},
|
||||
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
|
||||
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
|
||||
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
|
||||
{'name': 'Section ID', 'type': 'int', 'value': 'section_id',
|
||||
'description': 'The unique identifier for the library.'},
|
||||
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key',
|
||||
'description': 'The unique identifier for the movie, episode, or track.'},
|
||||
{'name': 'Parent Rating Key', 'type': 'int', 'value': 'parent_rating_key',
|
||||
'description': 'The unique identifier for the season or album.'},
|
||||
{'name': 'Grandparent Rating Key', 'type': 'int', 'value': 'grandparent_rating_key',
|
||||
'description': 'The unique identifier for the TV show or artist.'},
|
||||
{'name': 'Art', 'type': 'str', 'value': 'art', 'description': 'The Plex background art for the media.'},
|
||||
{'name': 'Thumb', 'type': 'str', 'value': 'thumb',
|
||||
'description': 'The Plex thumbnail for the movie or episode.'},
|
||||
{'name': 'Parent Thumb', 'type': 'str', 'value': 'parent_thumb',
|
||||
'description': 'The Plex thumbnail for the season or album.'},
|
||||
{'name': 'Grandparent Thumb', 'type': 'str', 'value': 'grandparent_thumb',
|
||||
'description': 'The Plex thumbnail for the TV show or artist.'},
|
||||
{'name': 'Poster Thumb', 'type': 'str', 'value': 'poster_thumb',
|
||||
'description': 'The Plex thumbnail for the poster image.'},
|
||||
{'name': 'Poster Title', 'type': 'str', 'value': 'poster_title',
|
||||
'description': 'The title for the poster image.'},
|
||||
{'name': 'Indexes', 'type': 'int', 'value': 'indexes',
|
||||
'description': 'If the media has video preview thumbnails.', 'example': '0 or 1'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Plex Remote Access',
|
||||
'parameters': [
|
||||
{'name': 'Remote Access Mapping State', 'type': 'str', 'value': 'remote_access_mapping_state',
|
||||
'description': 'The mapping state of the Plex remote access port.'},
|
||||
{'name': 'Remote Access Mapping Error', 'type': 'str', 'value': 'remote_access_mapping_error',
|
||||
'description': 'The mapping error of the Plex remote access port.'},
|
||||
{'name': 'Remote Access Public IP Address', 'type': 'str', 'value': 'remote_access_public_address',
|
||||
'description': 'The Plex remote access public IP address.'},
|
||||
{'name': 'Remote Access Public Port', 'type': 'str', 'value': 'remote_access_public_port',
|
||||
'description': 'The Plex remote access public port.'},
|
||||
{'name': 'Remote Access Private IP Address', 'type': 'str', 'value': 'remote_access_private_address',
|
||||
'description': 'The Plex remote access private IP address.'},
|
||||
{'name': 'Remote Access Private Port', 'type': 'str', 'value': 'remote_access_private_port',
|
||||
'description': 'The Plex remote access private port.'},
|
||||
{'name': 'Remote Access Failure Reason', 'type': 'str', 'value': 'remote_access_reason',
|
||||
'description': 'The failure reason for Plex remote access going down.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Plex Update Available',
|
||||
'parameters': [
|
||||
{'name': 'Update Version', 'type': 'str', 'value': 'update_version',
|
||||
'description': 'The available update version for your Plex Server.'},
|
||||
{'name': 'Update Url', 'type': 'str', 'value': 'update_url',
|
||||
'description': 'The download URL for the available update.'},
|
||||
{'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date',
|
||||
'description': 'The release date of the available update.'},
|
||||
{'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.',
|
||||
'example': 'Public or Plex Pass'},
|
||||
{'name': 'Update Platform', 'type': 'str', 'value': 'update_platform',
|
||||
'description': 'The platform of your Plex Server.'},
|
||||
{'name': 'Update Distro', 'type': 'str', 'value': 'update_distro',
|
||||
'description': 'The distro of your Plex Server.'},
|
||||
{'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build',
|
||||
'description': 'The distro build of your Plex Server.'},
|
||||
{'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements',
|
||||
'description': 'The requirements for the available update.'},
|
||||
{'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info',
|
||||
'description': 'Any extra info for the available update.'},
|
||||
{'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added',
|
||||
'description': 'The added changelog for the available update.'},
|
||||
{'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed',
|
||||
'description': 'The fixed changelog for the available update.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Tautulli Update Available',
|
||||
'parameters': [
|
||||
{'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version',
|
||||
'description': 'The available update version for Tautulli.'},
|
||||
{'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url',
|
||||
'description': 'The release page URL on GitHub.'},
|
||||
{'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar',
|
||||
'description': 'The tar download URL for the available update.'},
|
||||
{'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip',
|
||||
'description': 'The zip download URL for the available update.'},
|
||||
{'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit',
|
||||
'description': 'The commit hash for the available update.'},
|
||||
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind',
|
||||
'description': 'The number of commits behind for the available update.'},
|
||||
{'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog',
|
||||
'description': 'The changelog for the available update.'},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
NEWSLETTER_PARAMETERS = [
|
||||
{
|
||||
'category': 'Global',
|
||||
'parameters': [
|
||||
{'name': 'Server Name', 'type': 'str', 'value': 'server_name',
|
||||
'description': 'The name of your Plex Server.'},
|
||||
{'name': 'Start Date', 'type': 'str', 'value': 'start_date',
|
||||
'description': 'The start date of the newsletter.'},
|
||||
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'},
|
||||
{'name': 'Current Year', 'type': 'int', 'value': 'current_year',
|
||||
'description': 'The year of the start date of the newsletter.'},
|
||||
{'name': 'Current Month', 'type': 'int', 'value': 'current_month',
|
||||
'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'},
|
||||
{'name': 'Current Day', 'type': 'int', 'value': 'current_day',
|
||||
'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'},
|
||||
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour',
|
||||
'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'},
|
||||
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute',
|
||||
'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'},
|
||||
{'name': 'Current Second', 'type': 'int', 'value': 'current_second',
|
||||
'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'},
|
||||
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday',
|
||||
'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'},
|
||||
{'name': 'Current Week', 'type': 'int', 'value': 'current_week',
|
||||
'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'},
|
||||
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame',
|
||||
'description': 'The time frame included in the newsletter.'},
|
||||
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units',
|
||||
'description': 'The time frame units included in the newsletter.'},
|
||||
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url',
|
||||
'description': 'The self-hosted URL to the newsletter.'},
|
||||
{'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url',
|
||||
'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'},
|
||||
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid',
|
||||
'description': 'The unique identifier for the newsletter.'},
|
||||
{'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id',
|
||||
'description': 'The unique ID number for the newsletter agent.'},
|
||||
{'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name',
|
||||
'description': 'The unique ID name for the newsletter agent.'},
|
||||
{'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password',
|
||||
'description': 'The password required to view the newsletter if enabled.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Recently Added',
|
||||
'parameters': [
|
||||
{'name': 'Included Libraries', 'type': 'str', 'value': 'newsletter_libraries',
|
||||
'description': 'The list of libraries included in the newsletter.'},
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -13,25 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import object
|
||||
from future.builtins import str
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import threading
|
||||
import time
|
||||
|
||||
from configobj import ConfigObj, ParseError
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import helpers
|
||||
import logger
|
||||
else:
|
||||
from plexpy import helpers
|
||||
from plexpy import logger
|
||||
import jellypy
|
||||
from jellypy import helpers
|
||||
from jellypy import logger
|
||||
|
||||
|
||||
def bool_int(value):
|
||||
@@ -89,6 +81,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'CHECK_GITHUB': (int, 'General', 1),
|
||||
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
|
||||
'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
|
||||
'CHECK_GITHUB_CACHE_SECONDS': (int, 'Advanced', 3600),
|
||||
'CLEANUP_FILES': (int, 'General', 0),
|
||||
'CLOUDINARY_CLOUD_NAME': (str, 'Cloudinary', ''),
|
||||
'CLOUDINARY_API_KEY': (str, 'Cloudinary', ''),
|
||||
@@ -96,6 +89,8 @@ _CONFIG_DEFINITIONS = {
|
||||
'CONFIG_VERSION': (int, 'Advanced', 0),
|
||||
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
|
||||
'ENABLE_HTTPS': (int, 'General', 0),
|
||||
'EXPORT_DIR': (str, 'General', ''),
|
||||
'EXPORT_THREADS': (int, 'Advanced', 8),
|
||||
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
||||
'FREEZE_DB': (int, 'General', 0),
|
||||
'GET_FILE_SIZES': (int, 'General', 0),
|
||||
@@ -111,7 +106,8 @@ _CONFIG_DEFINITIONS = {
|
||||
'HOME_SECTIONS': (list, 'General', ['current_activity', 'watch_stats', 'library_stats', 'recently_added']),
|
||||
'HOME_LIBRARY_CARDS': (list, 'General', ['first_run']),
|
||||
'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music',
|
||||
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
|
||||
'popular_music', 'last_watched', 'top_users', 'top_platforms',
|
||||
'most_concurrent']),
|
||||
'HOME_REFRESH_INTERVAL': (int, 'General', 10),
|
||||
'HTTPS_CREATE_CERT': (int, 'General', 1),
|
||||
'HTTPS_CERT': (str, 'General', ''),
|
||||
@@ -131,6 +127,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'HTTP_USERNAME': (str, 'General', ''),
|
||||
'HTTP_PLEX_ADMIN': (int, 'General', 0),
|
||||
'HTTP_BASE_URL': (str, 'General', ''),
|
||||
'HTTP_RATE_LIMIT_ATTEMPTS': (int, 'General', 10),
|
||||
'HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL': (int, 'General', 300),
|
||||
'HTTP_RATE_LIMIT_LOCKOUT_TIME': (int, 'General', 300),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'IMGUR_CLIENT_ID': (str, 'Monitoring', ''),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'WAL'),
|
||||
@@ -161,8 +160,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 300),
|
||||
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
|
||||
'NOTIFY_REMOTE_ACCESS_THRESHOLD': (int, 'Monitoring', 60),
|
||||
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
||||
'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2),
|
||||
'NOTIFY_NEW_DEVICE_INITIAL_ONLY': (int, 'Monitoring', 1),
|
||||
'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
|
||||
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
|
||||
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
|
||||
@@ -194,7 +195,7 @@ _WHITELIST_KEYS = ['HTTPS_KEY']
|
||||
|
||||
_DO_NOT_IMPORT_KEYS = [
|
||||
'FIRST_RUN_COMPLETE', 'GET_FILE_SIZES_HOLD', 'GIT_PATH', 'PMS_LOGS_FOLDER',
|
||||
'BACKUP_DIR', 'CACHE_DIR', 'LOG_DIR', 'NEWSLETTER_DIR', 'NEWSLETTER_CUSTOM_DIR',
|
||||
'BACKUP_DIR', 'CACHE_DIR', 'EXPORT_DIR', 'LOG_DIR', 'NEWSLETTER_DIR', 'NEWSLETTER_CUSTOM_DIR',
|
||||
'HTTP_HOST', 'HTTP_PORT', 'HTTP_ROOT',
|
||||
'HTTP_USERNAME', 'HTTP_PASSWORD', 'HTTP_HASH_PASSWORD', 'HTTP_HASHED_PASSWORD',
|
||||
'ENABLE_HTTPS', 'HTTPS_CREATE_CERT', 'HTTPS_CERT', 'HTTPS_CERT_CHAIN', 'HTTPS_KEY'
|
||||
@@ -249,20 +250,20 @@ def import_tautulli_config(config=None, backup=False):
|
||||
# Remove keys that should not be imported
|
||||
for key in _DO_NOT_IMPORT_KEYS:
|
||||
delattr(imported_config, key)
|
||||
if plexpy.DOCKER:
|
||||
if jellypy.DOCKER or jellypy.SNAP:
|
||||
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
|
||||
delattr(imported_config, key)
|
||||
|
||||
# Merge the imported config file into the current config file
|
||||
plexpy.CONFIG._config.merge(imported_config._config)
|
||||
plexpy.CONFIG.write()
|
||||
jellypy.CONFIG._config.merge(imported_config._config)
|
||||
jellypy.CONFIG.write()
|
||||
|
||||
logger.info("Tautulli Config :: Tautulli config import complete.")
|
||||
set_import_thread(None)
|
||||
set_is_importing(False)
|
||||
|
||||
# Restart to apply changes
|
||||
plexpy.SIGNAL = 'restart'
|
||||
jellypy.SIGNAL = 'restart'
|
||||
|
||||
|
||||
def make_backup(cleanup=False, scheduler=False):
|
||||
@@ -272,15 +273,15 @@ def make_backup(cleanup=False, scheduler=False):
|
||||
backup_file = 'config.backup-{}.sched.ini'.format(helpers.now())
|
||||
else:
|
||||
backup_file = 'config.backup-{}.ini'.format(helpers.now())
|
||||
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
||||
backup_folder = jellypy.CONFIG.BACKUP_DIR
|
||||
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||
|
||||
# In case the user has deleted it manually
|
||||
if not os.path.exists(backup_folder):
|
||||
os.makedirs(backup_folder)
|
||||
|
||||
plexpy.CONFIG.write()
|
||||
shutil.copyfile(plexpy.CONFIG_FILE, backup_file_fp)
|
||||
jellypy.CONFIG.write()
|
||||
shutil.copyfile(jellypy.CONFIG_FILE, backup_file_fp)
|
||||
|
||||
if cleanup:
|
||||
now = time.time()
|
||||
@@ -288,17 +289,17 @@ def make_backup(cleanup=False, scheduler=False):
|
||||
for root, dirs, files in os.walk(backup_folder):
|
||||
ini_files = [os.path.join(root, f) for f in files if f.endswith('.sched.ini')]
|
||||
for file_ in ini_files:
|
||||
if os.stat(file_).st_mtime < now - plexpy.CONFIG.BACKUP_DAYS * 86400:
|
||||
if os.stat(file_).st_mtime < now - jellypy.CONFIG.BACKUP_DAYS * 86400:
|
||||
try:
|
||||
os.remove(file_)
|
||||
except OSError as e:
|
||||
logger.error("Tautulli Config :: Failed to delete %s from the backup folder: %s" % (file_, e))
|
||||
|
||||
if backup_file in os.listdir(backup_folder):
|
||||
logger.debug("Tautulli Config :: Successfully backed up %s to %s" % (plexpy.CONFIG_FILE, backup_file))
|
||||
logger.debug("Tautulli Config :: Successfully backed up %s to %s" % (jellypy.CONFIG_FILE, backup_file))
|
||||
return True
|
||||
else:
|
||||
logger.error("Tautulli Config :: Failed to backup %s to %s" % (plexpy.CONFIG_FILE, backup_file))
|
||||
logger.error("Tautulli Config :: Failed to backup %s to %s" % (jellypy.CONFIG_FILE, backup_file))
|
||||
return False
|
||||
|
||||
|
||||
@@ -329,7 +330,7 @@ class Config(object):
|
||||
for key, subkeys in self._config.items():
|
||||
for subkey, value in subkeys.items():
|
||||
if isinstance(value, str) and len(value.strip()) > 5 and \
|
||||
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
|
||||
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
|
||||
blacklist.add(value.strip())
|
||||
|
||||
logger._BLACKLIST_WORDS.update(blacklist)
|
||||
@@ -522,7 +523,7 @@ class Config(object):
|
||||
self.CONFIG_VERSION = 14
|
||||
|
||||
if self.CONFIG_VERSION == 14:
|
||||
if plexpy.DOCKER:
|
||||
if jellypy.DOCKER:
|
||||
self.PLEXPY_AUTO_UPDATE = 0
|
||||
|
||||
self.CONFIG_VERSION = 15
|
||||
@@ -532,3 +533,9 @@ class Config(object):
|
||||
self.JWT_UPDATE_SECRET = True
|
||||
|
||||
self.CONFIG_VERSION = 16
|
||||
|
||||
if self.CONFIG_VERSION == 16:
|
||||
if jellypy.SNAP:
|
||||
self.PLEXPY_AUTO_UPDATE = 0
|
||||
|
||||
self.CONFIG_VERSION = 17
|
||||
@@ -13,24 +13,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
from future.builtins import object
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import shutil
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import helpers
|
||||
import logger
|
||||
else:
|
||||
from plexpy import helpers
|
||||
from plexpy import logger
|
||||
import jellypy
|
||||
|
||||
from jellypy import helpers
|
||||
from jellypy import logger
|
||||
|
||||
FILENAME = "tautulli.db"
|
||||
db_lock = threading.Lock()
|
||||
@@ -216,6 +208,11 @@ def delete_recently_added():
|
||||
return clear_table('recently_added')
|
||||
|
||||
|
||||
def delete_exports():
|
||||
logger.info("Tautulli Database :: Clearing exported items from database.")
|
||||
return clear_table('exports')
|
||||
|
||||
|
||||
def delete_rows_from_table(table, row_ids):
|
||||
if row_ids and isinstance(row_ids, str):
|
||||
row_ids = list(map(helpers.cast_to_int, row_ids.split(',')))
|
||||
@@ -223,7 +220,7 @@ def delete_rows_from_table(table, row_ids):
|
||||
if row_ids:
|
||||
logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
|
||||
|
||||
# SQlite verions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
|
||||
# SQlite versions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
|
||||
# https://sqlite.org/limits.html
|
||||
sqlite_max_variable_number = 999
|
||||
|
||||
@@ -286,13 +283,24 @@ def vacuum():
|
||||
|
||||
|
||||
def optimize():
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
logger.info("Tautulli Database :: Optimizing database.")
|
||||
try:
|
||||
monitor_db.action('PRAGMA optimize')
|
||||
except Exception as e:
|
||||
logger.error("Tautulli Database :: Failed to optimize database: %s" % e)
|
||||
|
||||
|
||||
def optimize_db():
|
||||
vacuum()
|
||||
optimize()
|
||||
|
||||
|
||||
def db_filename(filename=FILENAME):
|
||||
""" Returns the filepath to the db """
|
||||
|
||||
return os.path.join(plexpy.DATA_DIR, filename)
|
||||
return os.path.join(jellypy.DATA_DIR, filename)
|
||||
|
||||
|
||||
def make_backup(cleanup=False, scheduler=False):
|
||||
@@ -304,13 +312,13 @@ def make_backup(cleanup=False, scheduler=False):
|
||||
corrupt = ''
|
||||
if not integrity:
|
||||
corrupt = '.corrupt'
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpydbcorrupt'})
|
||||
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpydbcorrupt'})
|
||||
|
||||
if scheduler:
|
||||
backup_file = 'tautulli.backup-{}{}.sched.db'.format(helpers.now(), corrupt)
|
||||
else:
|
||||
backup_file = 'tautulli.backup-{}{}.db'.format(helpers.now(), corrupt)
|
||||
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
||||
backup_folder = jellypy.CONFIG.BACKUP_DIR
|
||||
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||
|
||||
# In case the user has deleted it manually
|
||||
@@ -329,7 +337,7 @@ def make_backup(cleanup=False, scheduler=False):
|
||||
for root, dirs, files in os.walk(backup_folder):
|
||||
db_files = [os.path.join(root, f) for f in files if f.endswith('.sched.db')]
|
||||
for file_ in db_files:
|
||||
if os.stat(file_).st_mtime < now - plexpy.CONFIG.BACKUP_DAYS * 86400:
|
||||
if os.stat(file_).st_mtime < now - jellypy.CONFIG.BACKUP_DAYS * 86400:
|
||||
try:
|
||||
os.remove(file_)
|
||||
except OSError as e:
|
||||
@@ -345,10 +353,10 @@ def make_backup(cleanup=False, scheduler=False):
|
||||
|
||||
def get_cache_size():
|
||||
# This will protect against typecasting problems produced by empty string and None settings
|
||||
if not plexpy.CONFIG.CACHE_SIZEMB:
|
||||
if not jellypy.CONFIG.CACHE_SIZEMB:
|
||||
# sqlite will work with this (very slowly)
|
||||
return 0
|
||||
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
||||
return int(jellypy.CONFIG.CACHE_SIZEMB)
|
||||
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
@@ -365,9 +373,9 @@ class MonitorDatabase(object):
|
||||
self.filename = filename
|
||||
self.connection = sqlite3.connect(db_filename(filename), timeout=20)
|
||||
# Set database synchronous mode (default NORMAL)
|
||||
self.connection.execute("PRAGMA synchronous = %s" % plexpy.CONFIG.SYNCHRONOUS_MODE)
|
||||
self.connection.execute("PRAGMA synchronous = %s" % jellypy.CONFIG.SYNCHRONOUS_MODE)
|
||||
# Set database journal mode (default WAL)
|
||||
self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE)
|
||||
self.connection.execute("PRAGMA journal_mode = %s" % jellypy.CONFIG.JOURNAL_MODE)
|
||||
# Set database cache size (default 32MB)
|
||||
self.connection.execute("PRAGMA cache_size = -%s" % (get_cache_size() * 1024))
|
||||
self.connection.row_factory = dict_factory
|
||||
@@ -439,8 +447,9 @@ class MonitorDatabase(object):
|
||||
if self.connection.total_changes == changes_before:
|
||||
trans_type = 'insert'
|
||||
insert_query = (
|
||||
"INSERT INTO " + table_name + " (" + ", ".join(list(value_dict.keys()) + list(key_dict.keys())) + ")" +
|
||||
" VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")"
|
||||
"INSERT INTO " + table_name + " (" + ", ".join(
|
||||
list(value_dict.keys()) + list(key_dict.keys())) + ")" +
|
||||
" VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")"
|
||||
)
|
||||
try:
|
||||
self.action(insert_query, list(value_dict.values()) + list(key_dict.values()))
|
||||
@@ -454,4 +463,4 @@ class MonitorDatabase(object):
|
||||
# Get the last insert row id
|
||||
result = self.select_single(query='SELECT last_insert_rowid() AS last_id')
|
||||
if result:
|
||||
return result.get('last_id', None)
|
||||
return result.get('last_id', None)
|
||||
@@ -15,32 +15,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import next
|
||||
from future.builtins import str
|
||||
from future.builtins import object
|
||||
|
||||
import json
|
||||
from itertools import groupby
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import common
|
||||
import database
|
||||
import datatables
|
||||
import helpers
|
||||
import logger
|
||||
import pmsconnect
|
||||
import session
|
||||
else:
|
||||
from plexpy import common
|
||||
from plexpy import database
|
||||
from plexpy import datatables
|
||||
from plexpy import helpers
|
||||
from plexpy import logger
|
||||
from plexpy import pmsconnect
|
||||
from plexpy import session
|
||||
import jellypy
|
||||
from jellypy import common
|
||||
from jellypy import database
|
||||
from jellypy import datatables
|
||||
from jellypy import helpers
|
||||
from jellypy import logger
|
||||
from jellypy import session
|
||||
|
||||
|
||||
class DataFactory(object):
|
||||
@@ -51,14 +34,17 @@ class DataFactory(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_datatables_history(self, kwargs=None, custom_where=None, grouping=None):
|
||||
def get_datatables_history(self, kwargs=None, custom_where=None, grouping=None, include_activity=None):
|
||||
data_tables = datatables.DataTables()
|
||||
|
||||
if custom_where is None:
|
||||
custon_where = []
|
||||
custom_where = []
|
||||
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||
|
||||
if include_activity is None:
|
||||
include_activity = jellypy.CONFIG.HISTORY_TABLE_ACTIVITY
|
||||
|
||||
if session.get_session_user_id():
|
||||
session_user_id = str(session.get_session_user_id())
|
||||
@@ -96,6 +82,7 @@ class DataFactory(object):
|
||||
'product',
|
||||
'player',
|
||||
'ip_address',
|
||||
'machine_id',
|
||||
'session_history.media_type',
|
||||
'session_history_metadata.rating_key',
|
||||
'session_history_metadata.parent_rating_key',
|
||||
@@ -123,9 +110,9 @@ class DataFactory(object):
|
||||
'GROUP_CONCAT(session_history.id) AS group_ids',
|
||||
'NULL AS state',
|
||||
'NULL AS session_key'
|
||||
]
|
||||
]
|
||||
|
||||
if plexpy.CONFIG.HISTORY_TABLE_ACTIVITY:
|
||||
if include_activity:
|
||||
table_name_union = 'sessions'
|
||||
# Very hacky way to match the custom where parameters for the unioned table
|
||||
custom_where_union = [[c[0].split('.')[-1], c[1]] for c in custom_where]
|
||||
@@ -148,6 +135,7 @@ class DataFactory(object):
|
||||
'product',
|
||||
'player',
|
||||
'ip_address',
|
||||
'machine_id',
|
||||
'media_type',
|
||||
'rating_key',
|
||||
'parent_rating_key',
|
||||
@@ -175,7 +163,7 @@ class DataFactory(object):
|
||||
'NULL AS group_ids',
|
||||
'state',
|
||||
'session_key'
|
||||
]
|
||||
]
|
||||
|
||||
else:
|
||||
table_name_union = None
|
||||
@@ -213,11 +201,11 @@ class DataFactory(object):
|
||||
filter_duration = 0
|
||||
total_duration = self.get_total_duration(custom_where=custom_where)
|
||||
|
||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||
watched_percent = {'movie': jellypy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': jellypy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': jellypy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||
'photo': 0,
|
||||
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||
'clip': jellypy.CONFIG.TV_WATCHED_PERCENT
|
||||
}
|
||||
|
||||
rows = []
|
||||
@@ -260,6 +248,7 @@ class DataFactory(object):
|
||||
'player': item['player'],
|
||||
'ip_address': item['ip_address'],
|
||||
'live': item['live'],
|
||||
'machine_id': item['machine_id'],
|
||||
'media_type': item['media_type'],
|
||||
'rating_key': item['rating_key'],
|
||||
'parent_rating_key': item['parent_rating_key'],
|
||||
@@ -290,23 +279,26 @@ class DataFactory(object):
|
||||
'recordsTotal': query['totalCount'],
|
||||
'data': session.friendly_name_to_username(rows),
|
||||
'draw': query['draw'],
|
||||
'filter_duration': helpers.human_duration(filter_duration, sig='dhm'),
|
||||
'total_duration': helpers.human_duration(total_duration, sig='dhm')
|
||||
'filter_duration': helpers.human_duration(filter_duration, units='s'),
|
||||
'total_duration': helpers.human_duration(total_duration, units='s')
|
||||
}
|
||||
|
||||
return dict
|
||||
|
||||
def get_home_stats(self, grouping=None, time_range=30, stats_type='plays', stats_count=10, stats_cards=None):
|
||||
def get_home_stats(self, grouping=None, time_range=30, stats_type='plays',
|
||||
stats_start=0, stats_count=10, stat_id='', stats_cards=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if stat_id:
|
||||
stats_cards = [stat_id]
|
||||
if grouping is None:
|
||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||
if stats_cards is None:
|
||||
stats_cards = plexpy.CONFIG.HOME_STATS_CARDS
|
||||
stats_cards = jellypy.CONFIG.HOME_STATS_CARDS
|
||||
|
||||
movie_watched_percent = plexpy.CONFIG.MOVIE_WATCHED_PERCENT
|
||||
tv_watched_percent = plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||
music_watched_percent = plexpy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||
movie_watched_percent = jellypy.CONFIG.MOVIE_WATCHED_PERCENT
|
||||
tv_watched_percent = jellypy.CONFIG.TV_WATCHED_PERCENT
|
||||
music_watched_percent = jellypy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||
|
||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||
sort_type = 'total_duration' if stats_type == 'duration' else 'total_plays'
|
||||
@@ -331,10 +323,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.full_title, t.year ' \
|
||||
'ORDER BY %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_movies: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_movies: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -383,10 +376,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.full_title, t.year ' \
|
||||
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_movies: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_movies: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -432,10 +426,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.grandparent_title ' \
|
||||
'ORDER BY %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_tv: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_tv: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -484,10 +479,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.grandparent_title ' \
|
||||
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_tv: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_tv: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -534,10 +530,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.original_title, t.grandparent_title ' \
|
||||
'ORDER BY %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_music: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_music: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -587,10 +584,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.original_title, t.grandparent_title ' \
|
||||
'ORDER BY users_watched DESC, %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_music: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_music: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -637,10 +635,11 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.user_id ' \
|
||||
'ORDER BY %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_users: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_users: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -665,7 +664,7 @@ class DataFactory(object):
|
||||
'title': '',
|
||||
'platform': '',
|
||||
'row_id': ''
|
||||
}
|
||||
}
|
||||
top_users.append(row)
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
@@ -689,16 +688,18 @@ class DataFactory(object):
|
||||
' GROUP BY %s) AS t ' \
|
||||
'GROUP BY t.platform ' \
|
||||
'ORDER BY %s DESC, started DESC ' \
|
||||
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
|
||||
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_platforms: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_platforms: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
# Rename Mystery platform names
|
||||
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
||||
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default')
|
||||
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()),
|
||||
'default')
|
||||
|
||||
row = {'total_plays': item['total_plays'],
|
||||
'total_duration': item['total_duration'],
|
||||
@@ -746,10 +747,12 @@ class DataFactory(object):
|
||||
' OR t.media_type == "episode" AND percent_complete >= %s ' \
|
||||
'GROUP BY t.id ' \
|
||||
'ORDER BY last_watch DESC ' \
|
||||
'LIMIT %s' % (time_range, group_by, movie_watched_percent, tv_watched_percent, stats_count)
|
||||
'LIMIT %s OFFSET %s' % (time_range, group_by, movie_watched_percent, tv_watched_percent,
|
||||
stats_count, stats_start)
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: last_watched: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: last_watched: %s." % e)
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
@@ -838,32 +841,35 @@ class DataFactory(object):
|
||||
|
||||
title = 'Concurrent Transcodes'
|
||||
query = base_query \
|
||||
+ 'AND session_history_media_info.transcode_decision = "transcode" '
|
||||
+ 'AND session_history_media_info.transcode_decision = "transcode" '
|
||||
result = monitor_db.select(query)
|
||||
if result:
|
||||
most_concurrent.append(calc_most_concurrent(title, result))
|
||||
|
||||
title = 'Concurrent Direct Streams'
|
||||
query = base_query \
|
||||
+ 'AND session_history_media_info.transcode_decision = "copy" '
|
||||
+ 'AND session_history_media_info.transcode_decision = "copy" '
|
||||
result = monitor_db.select(query)
|
||||
if result:
|
||||
most_concurrent.append(calc_most_concurrent(title, result))
|
||||
|
||||
title = 'Concurrent Direct Plays'
|
||||
query = base_query \
|
||||
+ 'AND session_history_media_info.transcode_decision = "direct play" '
|
||||
+ 'AND session_history_media_info.transcode_decision = "direct play" '
|
||||
result = monitor_db.select(query)
|
||||
if result:
|
||||
most_concurrent.append(calc_most_concurrent(title, result))
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: most_concurrent: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: most_concurrent: %s." % e)
|
||||
return None
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
'stat_title': 'Most Concurrent Streams',
|
||||
'rows': most_concurrent})
|
||||
|
||||
if stat_id and home_stats:
|
||||
return home_stats[0]
|
||||
return home_stats
|
||||
|
||||
def get_library_stats(self, library_cards=[]):
|
||||
@@ -1565,14 +1571,14 @@ class DataFactory(object):
|
||||
|
||||
key = item['parent_media_index'] if match_type == 'index' else item['parent_title']
|
||||
parents.update({key:
|
||||
{'rating_key': item['parent_rating_key'],
|
||||
'children': children}
|
||||
{'rating_key': item['parent_rating_key'],
|
||||
'children': children}
|
||||
})
|
||||
|
||||
key = 0 if match_type == 'index' else item['grandparent_title']
|
||||
grandparents.update({key:
|
||||
{'rating_key': item['grandparent_rating_key'],
|
||||
'children': parents}
|
||||
{'rating_key': item['grandparent_rating_key'],
|
||||
'children': parents}
|
||||
})
|
||||
|
||||
key_list = grandparents
|
||||
@@ -1580,7 +1586,7 @@ class DataFactory(object):
|
||||
return key_list
|
||||
|
||||
def update_metadata(self, old_key_list='', new_key_list='', media_type=''):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
# TODO: pms_connect = pmsconnect.PmsConnect()
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
# function to map rating keys pairs
|
||||
@@ -1599,33 +1605,38 @@ class DataFactory(object):
|
||||
if old_key_list and new_key_list:
|
||||
mapping = get_pairs(old_key_list, new_key_list)
|
||||
|
||||
if mapping:
|
||||
logger.info("Tautulli DataFactory :: Updating metadata in the database.")
|
||||
for old_key, new_key in mapping.items():
|
||||
metadata = pms_connect.get_metadata_details(new_key)
|
||||
|
||||
if metadata:
|
||||
if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
|
||||
# check grandparent_rating_key (2 tables)
|
||||
monitor_db.action('UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
||||
[new_key, old_key])
|
||||
monitor_db.action('UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
||||
[new_key, old_key])
|
||||
elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album':
|
||||
# check parent_rating_key (2 tables)
|
||||
monitor_db.action('UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
||||
[new_key, old_key])
|
||||
monitor_db.action('UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
||||
[new_key, old_key])
|
||||
else:
|
||||
# check rating_key (2 tables)
|
||||
monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?',
|
||||
[new_key, old_key])
|
||||
monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?',
|
||||
[new_key, old_key])
|
||||
|
||||
# update session_history_metadata table
|
||||
self.update_metadata_details(old_key, new_key, metadata)
|
||||
# TODO: Jellyfin
|
||||
# if mapping:
|
||||
# logger.info("Tautulli DataFactory :: Updating metadata in the database.")
|
||||
# for old_key, new_key in mapping.items():
|
||||
# metadata = pms_connect.get_metadata_details(new_key)
|
||||
#
|
||||
# if metadata:
|
||||
# if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
|
||||
# # check grandparent_rating_key (2 tables)
|
||||
# monitor_db.action(
|
||||
# 'UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
||||
# [new_key, old_key])
|
||||
# monitor_db.action(
|
||||
# 'UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
||||
# [new_key, old_key])
|
||||
# elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album':
|
||||
# # check parent_rating_key (2 tables)
|
||||
# monitor_db.action(
|
||||
# 'UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
||||
# [new_key, old_key])
|
||||
# monitor_db.action(
|
||||
# 'UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
||||
# [new_key, old_key])
|
||||
# else:
|
||||
# # check rating_key (2 tables)
|
||||
# monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?',
|
||||
# [new_key, old_key])
|
||||
# monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?',
|
||||
# [new_key, old_key])
|
||||
#
|
||||
# # update session_history_metadata table
|
||||
# self.update_metadata_details(old_key, new_key, metadata)
|
||||
|
||||
return 'Updated metadata in database.'
|
||||
else:
|
||||
@@ -1649,7 +1660,7 @@ class DataFactory(object):
|
||||
genres = ";".join(metadata['genres'])
|
||||
labels = ";".join(metadata['labels'])
|
||||
|
||||
#logger.info("Tautulli DataFactory :: Updating metadata in the database for rating key: %s." % new_rating_key)
|
||||
# logger.info("Tautulli DataFactory :: Updating metadata in the database for rating key: %s." % new_rating_key)
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
# Update the session_history_metadata table
|
||||
@@ -1829,12 +1840,22 @@ class DataFactory(object):
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for delete_newsletter_log: %s." % e)
|
||||
return False
|
||||
|
||||
def get_user_devices(self, user_id=''):
|
||||
def get_user_devices(self, user_id='', history_only=True):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if user_id:
|
||||
if history_only:
|
||||
query = 'SELECT machine_id FROM session_history ' \
|
||||
'WHERE user_id = ? ' \
|
||||
'GROUP BY machine_id'
|
||||
else:
|
||||
query = 'SELECT * FROM (' \
|
||||
'SELECT user_id, machine_id FROM session_history ' \
|
||||
'UNION SELECT user_id, machine_id from sessions_continued) ' \
|
||||
'WHERE user_id = ? ' \
|
||||
'GROUP BY machine_id'
|
||||
|
||||
try:
|
||||
query = 'SELECT machine_id FROM session_history WHERE user_id = ? GROUP BY machine_id'
|
||||
result = monitor_db.select(query=query, args=[user_id])
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_user_devices: %s." % e)
|
||||
@@ -1852,7 +1873,8 @@ class DataFactory(object):
|
||||
query = 'SELECT * FROM recently_added WHERE rating_key = ?'
|
||||
result = monitor_db.select(query=query, args=[rating_key])
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_recently_added_item: %s." % e)
|
||||
logger.warn(
|
||||
"Tautulli DataFactory :: Unable to execute database query for get_recently_added_item: %s." % e)
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
@@ -1862,23 +1884,24 @@ class DataFactory(object):
|
||||
def set_recently_added_item(self, rating_key=''):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key)
|
||||
|
||||
keys = {'rating_key': metadata['rating_key']}
|
||||
|
||||
values = {'added_at': metadata['added_at'],
|
||||
'section_id': metadata['section_id'],
|
||||
'parent_rating_key': metadata['parent_rating_key'],
|
||||
'grandparent_rating_key': metadata['grandparent_rating_key'],
|
||||
'media_type': metadata['media_type'],
|
||||
'media_info': json.dumps(metadata['media_info'])
|
||||
}
|
||||
|
||||
try:
|
||||
monitor_db.upsert(table_name='recently_added', key_dict=keys, value_dict=values)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for set_recently_added_item: %s." % e)
|
||||
return False
|
||||
# TODO: Jellyfin
|
||||
# pms_connect = pmsconnect.PmsConnect()
|
||||
# metadata = pms_connect.get_metadata_details(rating_key)
|
||||
#
|
||||
# keys = {'rating_key': metadata['rating_key']}
|
||||
#
|
||||
# values = {'added_at': metadata['added_at'],
|
||||
# 'section_id': metadata['section_id'],
|
||||
# 'parent_rating_key': metadata['parent_rating_key'],
|
||||
# 'grandparent_rating_key': metadata['grandparent_rating_key'],
|
||||
# 'media_type': metadata['media_type'],
|
||||
# 'media_info': json.dumps(metadata['media_info'])
|
||||
# }
|
||||
#
|
||||
# try:
|
||||
# monitor_db.upsert(table_name='recently_added', key_dict=keys, value_dict=values)
|
||||
# except Exception as e:
|
||||
# logger.warn("Tautulli DataFactory :: Unable to execute database query for set_recently_added_item: %s." % e)
|
||||
# return False
|
||||
|
||||
return True
|
||||
@@ -13,20 +13,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import object
|
||||
|
||||
import re
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import database
|
||||
import helpers
|
||||
import logger
|
||||
else:
|
||||
from plexpy import database
|
||||
from plexpy import helpers
|
||||
from plexpy import logger
|
||||
from jellypy import database
|
||||
from jellypy import helpers
|
||||
from jellypy import logger
|
||||
|
||||
|
||||
class DataTables(object):
|
||||
@@ -224,7 +215,7 @@ class DataTables(object):
|
||||
args.append('%' + search_param + '%')
|
||||
if where:
|
||||
where = 'WHERE ' + where.rstrip(' OR ')
|
||||
|
||||
|
||||
return where, args
|
||||
|
||||
# This method extracts column data from our column list
|
||||
@@ -13,10 +13,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class PlexPyException(Exception):
|
||||
class JellyPyException(Exception):
|
||||
"""
|
||||
Generic Tautulli Exception - should never be thrown, only subclassed
|
||||
"""
|
||||