Compare commits
284 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c82bb023a | ||
|
|
0a86f24095 | ||
|
|
b41249cfa8 | ||
|
|
6659802689 | ||
|
|
964c503223 | ||
|
|
15568bf20a | ||
|
|
d10cd324bb | ||
|
|
2a22ab8c33 | ||
|
|
ca736cdae2 | ||
|
|
d589c57dd2 | ||
|
|
9b0caf2a47 | ||
|
|
f8b00bbd67 | ||
|
|
91a8c0e7a0 | ||
|
|
2089172384 | ||
|
|
1ab87e5334 | ||
|
|
b5e6861032 | ||
|
|
189930918a | ||
|
|
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 | ||
|
|
34c9ede9c9 | ||
|
|
be9f06795d | ||
|
|
ea9904bd56 | ||
|
|
501f08dd5e | ||
|
|
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 | ||
|
|
86699ece8e | ||
|
|
e9f464e34d | ||
|
|
97775e2a3b | ||
|
|
54433c43e6 | ||
|
|
3376908710 | ||
|
|
284f77b9ae | ||
|
|
2fe49f316f | ||
|
|
1428a2485f | ||
|
|
3a1b4e34aa | ||
|
|
26fb9a6803 | ||
|
|
31e6f4282d | ||
|
|
6bc7de7a6d | ||
|
|
0203a1d4dc | ||
|
|
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 | ||
|
|
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 |
76
.github/workflows/publish-docker.yml
vendored
@@ -27,17 +27,21 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo ::set-output name=commit::${GITHUB_SHA}
|
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=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_platforms::linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||||
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
|
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: crazy-max/ghaction-docker-buildx@v3
|
uses: docker/setup-buildx-action@v1
|
||||||
with:
|
with:
|
||||||
buildx-version: latest
|
version: latest
|
||||||
|
|
||||||
- name: Cache Docker Layers
|
- name: Cache Docker Layers
|
||||||
id: cache
|
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
@@ -45,45 +49,39 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- name: Docker Buildx (no push)
|
- name: Login to DockerHub
|
||||||
run: |
|
uses: docker/login-action@v1
|
||||||
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()
|
if: success()
|
||||||
env:
|
with:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
run: |
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
|
||||||
|
|
||||||
- name: Docker Buildx (push)
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
with:
|
||||||
docker buildx build \
|
registry: ghcr.io
|
||||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
password: ${{ secrets.GHCR_TOKEN }}
|
||||||
--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
|
- name: Docker Build and Push
|
||||||
if: always()
|
uses: docker/build-push-action@v2
|
||||||
run: |
|
if: success()
|
||||||
rm -f ${HOME}/.docker/config.json
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: ${{ steps.prepare.outputs.docker_platforms }}
|
||||||
|
build-args: |
|
||||||
|
TAG=${{ steps.prepare.outputs.tag }}
|
||||||
|
BRANCH=${{ steps.prepare.outputs.branch }}
|
||||||
|
COMMIT=${{ steps.prepare.outputs.commit }}
|
||||||
|
BUILD_DATE=${{ steps.prepare.outputs.build_date }}
|
||||||
|
tags: |
|
||||||
|
${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}
|
||||||
|
ghcr.io/${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|
||||||
- name: Post Status to Discord
|
- name: Post Status to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
|
|||||||
4
.github/workflows/publish-release.yml
vendored
@@ -83,11 +83,11 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
echo ::set-env name=VERSION::${GITHUB_REF#refs/tags/v}
|
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
echo ::set-output 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/}
|
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||||
else
|
else
|
||||||
echo ::set-env name=VERSION::0.0.0
|
echo "VERSION=0.0.0" >> $GITHUB_ENV
|
||||||
echo ::set-output name=VERSION::0.0.0
|
echo ::set-output name=VERSION::0.0.0
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||||
fi
|
fi
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -17,6 +17,7 @@ version.lock
|
|||||||
logs/*
|
logs/*
|
||||||
backups/*
|
backups/*
|
||||||
cache/*
|
cache/*
|
||||||
|
exports/*
|
||||||
newsletters/*
|
newsletters/*
|
||||||
*.mmdb
|
*.mmdb
|
||||||
version.txt
|
version.txt
|
||||||
|
|||||||
51
CHANGELOG.md
@@ -1,5 +1,56 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## v2.5.6 (2020-10-02)
|
||||||
|
|
||||||
* Activity:
|
* Activity:
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ ENV TZ=UTC
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
|
groupadd -g 1000 tautulli && \
|
||||||
|
useradd -u 1000 -g 1000 tautulli && \
|
||||||
echo ${BRANCH} > /app/branch.txt && \
|
echo ${BRANCH} > /app/branch.txt && \
|
||||||
echo ${COMMIT} > /app/version.txt
|
echo ${COMMIT} > /app/version.txt
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
|
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
||||||
ENTRYPOINT [ "./start.sh" ]
|
ENTRYPOINT [ "./start.sh" ]
|
||||||
|
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
|||||||
[](https://python.org/downloads)
|
[](https://python.org/downloads)
|
||||||
[](https://hub.docker.com/r/tautulli/tautulli)
|
[](https://hub.docker.com/r/tautulli/tautulli)
|
||||||
[](https://hub.docker.com/r/tautulli/tautulli)
|
[](https://hub.docker.com/r/tautulli/tautulli)
|
||||||
|
[](https://github.com/Tautulli/Tautulli/releases/latest)
|
||||||
|
|
||||||
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
|
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" 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/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/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/opensans.min.css" 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.all.min.css" rel="stylesheet">
|
||||||
@@ -22,21 +24,21 @@
|
|||||||
${next.headIncludes()}
|
${next.headIncludes()}
|
||||||
|
|
||||||
<!-- Favicons -->
|
<!-- 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="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.0.5">
|
<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.0.5">
|
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||||
|
|
||||||
<!-- ICONS -->
|
<!-- ICONS -->
|
||||||
<!-- Android -->
|
<!-- 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">
|
<meta name="theme-color" content="#282a2d">
|
||||||
<!-- Apple -->
|
<!-- Apple -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
<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.0.5" color="#282a2d">
|
<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">
|
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||||
<!-- Microsoft -->
|
<!-- Microsoft -->
|
||||||
<meta name="application-name" content="Tautulli">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body class="content">
|
<body class="content">
|
||||||
@@ -48,17 +50,17 @@
|
|||||||
% if plexpy.UPDATE_AVAILABLE is None:
|
% if plexpy.UPDATE_AVAILABLE is None:
|
||||||
You are running an unknown version of Tautulli.<br />
|
You are running an unknown version of Tautulli.<br />
|
||||||
% elif plexpy.UPDATE_AVAILABLE == 'release':
|
% 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 />
|
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
|
||||||
% elif plexpy.UPDATE_AVAILABLE == 'commit':
|
% 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 />
|
newer version</a> of Tautulli is available!<br />
|
||||||
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
|
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
|
||||||
% endif
|
% endif
|
||||||
% if plexpy.INSTALL_TYPE == 'docker':
|
% if plexpy.INSTALL_TYPE == 'docker':
|
||||||
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
% elif plexpy.INSTALL_TYPE in ('windows', 'macos'):
|
% 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>
|
<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:
|
% else:
|
||||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
% endif
|
% endif
|
||||||
@@ -136,7 +138,7 @@
|
|||||||
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
|
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
|
||||||
<li role="separator" class="divider"></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="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><a href="support"><i class="fa fa-fw fa-comment"></i> Support</a></li>
|
||||||
<li role="separator" class="divider"></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>
|
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
|
||||||
@@ -202,7 +204,7 @@ ${next.modalIncludes()}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
||||||
@@ -239,7 +241,7 @@ ${next.modalIncludes()}
|
|||||||
<p>
|
<p>
|
||||||
Click the button below to continue to GitHub.
|
Click the button below to continue to GitHub.
|
||||||
</p>
|
</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
|
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i> Sponsor
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,15 +249,15 @@ ${next.modalIncludes()}
|
|||||||
<p>
|
<p>
|
||||||
Click the button below to continue to Patreon.
|
Click the button below to continue to Patreon.
|
||||||
</p>
|
</p>
|
||||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
|
<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" height="40">
|
<img src="images/become_a_patron_button.png" alt="Become a Patron" width="170" height="40">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
|
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
|
||||||
<p>
|
<p>
|
||||||
Click the button below to continue to PayPal.
|
Click the button below to continue to PayPal.
|
||||||
</p>
|
</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">
|
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,14 +290,17 @@ ${next.modalIncludes()}
|
|||||||
</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/bootstrap.min.js"></script>
|
||||||
<script src="${http_root}js/bootstrap-hover-dropdown.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/pnotify.custom.min.js"></script>
|
||||||
<script src="${http_root}js/platform.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/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/jquery.tripleclick.min.js"></script>
|
||||||
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||||
<script>
|
<script>
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -325,15 +330,15 @@ ${next.modalIncludes()}
|
|||||||
if (result.update === null) {
|
if (result.update === null) {
|
||||||
msg = 'You are running an unknown version of Tautulli.<br />';
|
msg = 'You are running an unknown version of Tautulli.<br />';
|
||||||
} else if (result.update === true && result.release === true) {
|
} 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) {
|
} 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 />';
|
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />';
|
||||||
}
|
}
|
||||||
if (result.install_type === 'docker') {
|
if (result.install_type === 'docker') {
|
||||||
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
|
} 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>'
|
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 {
|
} else {
|
||||||
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
}
|
}
|
||||||
@@ -441,12 +446,16 @@ ${next.modalIncludes()}
|
|||||||
data: $(this).serialize(),
|
data: $(this).serialize(),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
statusCode: {
|
statusCode: {
|
||||||
200: function() {
|
200: function(xhr, status) {
|
||||||
window.location = "${http_root}";
|
window.location = "${http_root}";
|
||||||
},
|
},
|
||||||
401: function() {
|
401: function(xhr, status) {
|
||||||
$('#incorrect-login').show();
|
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||||
$('#username').focus();
|
$('#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() {
|
complete: function() {
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ DOCUMENTATION :: END
|
|||||||
% if plexpy.CURRENT_VERSION:
|
% if plexpy.CURRENT_VERSION:
|
||||||
<tr>
|
<tr>
|
||||||
<td>Git Branch:</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Git Commit Hash:</td>
|
<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>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
@@ -49,6 +49,10 @@ DOCUMENTATION :: END
|
|||||||
<td>Cache Directory:</td>
|
<td>Cache Directory:</td>
|
||||||
<td>${plexpy.CONFIG.CACHE_DIR}</td>
|
<td>${plexpy.CONFIG.CACHE_DIR}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Export Directory:</td>
|
||||||
|
<td>${plexpy.CONFIG.EXPORT_DIR}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Newsletter Directory:</td>
|
<td>Newsletter Directory:</td>
|
||||||
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
|
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
|
||||||
@@ -74,19 +78,19 @@ DOCUMENTATION :: END
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="top-line">Resources:</td>
|
<td class="top-line">Resources:</td>
|
||||||
<td class="top-line">
|
<td class="top-line">
|
||||||
<a class="no-highlight" href="${anon_url('https://tautulli.com')}" target="_blank">Tautulli Website</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">GitHub Source</a> |
|
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" 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))}" data-id="issue">GitHub Issues</a> |
|
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" 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">GitHub Wiki</a> |
|
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" 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))}" data-id="feature request">FeatHub Feature Requests</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Support:</td>
|
<td>Support:</td>
|
||||||
<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://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">Tautulli Subreddit</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">Plex Forums</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ ul.ColVis_collection {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
padding: 8px 8px 4px 8px;
|
padding: 8px 8px 4px 8px;
|
||||||
margin: 10px 0px 0px 0px;
|
margin: 10px 0px 10px 0px;
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 2002;
|
z-index: 2002;
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ select.form-control:focus,
|
|||||||
.selectize-dropdown .optgroup-header {
|
.selectize-dropdown .optgroup-header {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.selectize-dropdown [data-selectable].option-disabled {
|
||||||
|
color: #aaa;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
select.form-control option {
|
select.form-control option {
|
||||||
color: #555;
|
color: #555;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
@@ -746,16 +750,17 @@ a .users-poster-face:hover {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
#dashboard-checking-activity,
|
#dashboard-checking-activity,
|
||||||
#dashboard-no-activity {
|
#dashboard-no-activity,
|
||||||
|
#dashboard-checking-recently-added,
|
||||||
|
#dashboard-no-recently-added {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.dashboard-activity-instance {
|
.dashboard-activity-instance {
|
||||||
float: left;
|
float: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 290px;
|
height: 290px;
|
||||||
min-width: 350px;
|
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin-right: 25px;
|
margin-left: 25px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
.dashboard-activity-container {
|
.dashboard-activity-container {
|
||||||
@@ -1156,9 +1161,8 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
|||||||
float: left;
|
float: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
min-width: 350px;
|
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin-right: 25px;
|
margin-left: 25px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
.dashboard-stats-container {
|
.dashboard-stats-container {
|
||||||
@@ -1444,9 +1448,6 @@ a:hover .dashboard-stats-square {
|
|||||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
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 {
|
.dashboard-recent-media-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -1750,6 +1751,7 @@ a:hover .dashboard-recent-media-cover {
|
|||||||
box-shadow: inset 0 0 0 2px #e9a049;
|
box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .2s;
|
transition: opacity .2s;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.summary-poster-face-overlay span {
|
.summary-poster-face-overlay span {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1963,7 +1965,10 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
|
|||||||
.item-children-instance {
|
.item-children-instance {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.item-children-instance.max-height {
|
||||||
|
max-height: 700px;
|
||||||
}
|
}
|
||||||
.item-children-instance li {
|
.item-children-instance li {
|
||||||
float: left;
|
float: left;
|
||||||
@@ -2099,7 +2104,7 @@ a:hover .item-children-poster {
|
|||||||
}
|
}
|
||||||
.item-children-list-item-title {
|
.item-children-list-item-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: calc(100% - 110px);
|
width: calc(100% - 125px);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -2109,9 +2114,17 @@ a:hover .item-children-poster {
|
|||||||
color: #777;
|
color: #777;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 40px;
|
width: 60px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
.nav-list {
|
||||||
|
float: left;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.nav-list.nav-pills > li > a {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
#new_title h3 {
|
#new_title h3 {
|
||||||
color: #E5A00D;
|
color: #E5A00D;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -2185,32 +2198,17 @@ li.advanced-setting {
|
|||||||
.user-info-username {
|
.user-info-username {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
padding-top: 27px;
|
padding-top: 15px;
|
||||||
padding-left: 105px;
|
padding-left: 105px;
|
||||||
}
|
}
|
||||||
.user-info-nav {
|
.user-info-nav {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
padding-left: 105px;
|
||||||
.user-info-nav > .active > a {
|
|
||||||
color: #cc7b19;
|
|
||||||
}
|
}
|
||||||
.nav-tabs > .active > a:hover,
|
.nav-tabs > .active > a:hover,
|
||||||
.nav-tabs > .active > a:focus {
|
.nav-tabs > .active > a:focus {
|
||||||
color: #e9a049;
|
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 {
|
||||||
}
|
}
|
||||||
.user-overview-stats-wrapper ul {
|
.user-overview-stats-wrapper ul {
|
||||||
@@ -3294,23 +3292,36 @@ pre::-webkit-scrollbar-track {
|
|||||||
pre::-webkit-scrollbar-thumb {
|
pre::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0,0,0,.15);
|
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
|
@media only screen
|
||||||
and (min-device-width: 300px)
|
and (min-width: 300px)
|
||||||
and (max-device-width: 450px) {
|
and (max-width: 450px) {
|
||||||
.home-platforms-instance {
|
.home-platforms-instance {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
.dashboard-activity-instance {
|
.dashboard-activity-instance,
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.dashboard-stats-instance {
|
.dashboard-stats-instance {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
#currentActivity,
|
||||||
|
#home-stats,
|
||||||
|
#library-stats {
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media only screen
|
@media only screen
|
||||||
and (min-device-width: 300px)
|
and (min-width: 300px)
|
||||||
and (max-device-width: 740px) {
|
and (max-width: 740px) {
|
||||||
.header-bar {
|
.header-bar {
|
||||||
display: block;
|
display: block;
|
||||||
float: none !important;
|
float: none !important;
|
||||||
@@ -3337,8 +3348,8 @@ pre::-webkit-scrollbar-thumb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media only screen
|
@media only screen
|
||||||
and (min-device-width: 740px)
|
and (min-width: 740px)
|
||||||
and (max-device-width: 1024px) {
|
and (max-width: 1024px) {
|
||||||
.button-bar {
|
.button-bar {
|
||||||
float: right !important;
|
float: right !important;
|
||||||
}
|
}
|
||||||
@@ -3485,6 +3496,9 @@ pre::-webkit-scrollbar-thumb {
|
|||||||
.selectize-input input[type='text'] {
|
.selectize-input input[type='text'] {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
.selectize-input.disabled, .selectize-input.disabled * {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
.small-muted {
|
.small-muted {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
color: #777;
|
color: #777;
|
||||||
@@ -3707,6 +3721,20 @@ a:hover .overlay-refresh-image {
|
|||||||
a:hover .overlay-refresh-image:hover {
|
a:hover .overlay-refresh-image:hover {
|
||||||
opacity: .9;
|
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 {
|
#ip_error, #isp_error {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -3821,19 +3849,19 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-position: center !important;
|
background-position: center !important;
|
||||||
}
|
}
|
||||||
.platform-android {
|
.platform-android {
|
||||||
background-color: #a4ca39;
|
background-color: #3ddc84;
|
||||||
background-image: url(../images/platforms/android.svg);
|
background-image: url(../images/platforms/android.svg);
|
||||||
}
|
}
|
||||||
.platform-atv {
|
.platform-atv {
|
||||||
background-color: #858487;
|
background-color: #a2aaad;
|
||||||
background-image: url(../images/platforms/atv.svg);
|
background-image: url(../images/platforms/atv.svg);
|
||||||
}
|
}
|
||||||
.platform-chrome {
|
.platform-chrome {
|
||||||
background-color: #ed5e50;
|
background-color: #db4437;
|
||||||
background-image: url(../images/platforms/chrome.svg);
|
background-image: url(../images/platforms/chrome.svg);
|
||||||
}
|
}
|
||||||
.platform-chromecast {
|
.platform-chromecast {
|
||||||
background-color: #10a4e8;
|
background-color: #4285f4;
|
||||||
background-image: url(../images/platforms/chromecast.svg);
|
background-image: url(../images/platforms/chromecast.svg);
|
||||||
}
|
}
|
||||||
.platform-default {
|
.platform-default {
|
||||||
@@ -3841,11 +3869,11 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-image: url(../images/platforms/default.svg);
|
background-image: url(../images/platforms/default.svg);
|
||||||
}
|
}
|
||||||
.platform-dlna {
|
.platform-dlna {
|
||||||
background-color: #0cb14b;
|
background-color: #4ba32f;
|
||||||
background-image: url(../images/platforms/dlna.svg);
|
background-image: url(../images/platforms/dlna.svg);
|
||||||
}
|
}
|
||||||
.platform-firefox {
|
.platform-firefox {
|
||||||
background-color: #e67817;
|
background-color: #ff7139;
|
||||||
background-image: url(../images/platforms/firefox.svg);
|
background-image: url(../images/platforms/firefox.svg);
|
||||||
}
|
}
|
||||||
.platform-gtv {
|
.platform-gtv {
|
||||||
@@ -3853,27 +3881,27 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-image: url(../images/platforms/gtv.svg);
|
background-image: url(../images/platforms/gtv.svg);
|
||||||
}
|
}
|
||||||
.platform-ie {
|
.platform-ie {
|
||||||
background-color: #00599e;
|
background-color: #18bcef;
|
||||||
background-image: url(../images/platforms/ie.svg);
|
background-image: url(../images/platforms/ie.svg);
|
||||||
}
|
}
|
||||||
.platform-ios {
|
.platform-ios {
|
||||||
background-color: #858487;
|
background-color: #a2aaad;
|
||||||
background-image: url(../images/platforms/ios.svg);
|
background-image: url(../images/platforms/ios.svg);
|
||||||
}
|
}
|
||||||
.platform-kodi {
|
.platform-kodi {
|
||||||
background-color: #31afe1;
|
background-color: #30aada;
|
||||||
background-image: url(../images/platforms/kodi.svg);
|
background-image: url(../images/platforms/kodi.svg);
|
||||||
}
|
}
|
||||||
.platform-lg {
|
.platform-lg {
|
||||||
background-color: #a50034;
|
background-color: #990033;
|
||||||
background-image: url(../images/platforms/lg.svg);
|
background-image: url(../images/platforms/lg.svg);
|
||||||
}
|
}
|
||||||
.platform-linux {
|
.platform-linux {
|
||||||
background-color: #1793d0;
|
background-color: #0099cc;
|
||||||
background-image: url(../images/platforms/linux.svg);
|
background-image: url(../images/platforms/linux.svg);
|
||||||
}
|
}
|
||||||
.platform-macos {
|
.platform-macos {
|
||||||
background-color: #858487;
|
background-color: #a2aaad;
|
||||||
background-image: url(../images/platforms/macos.svg);
|
background-image: url(../images/platforms/macos.svg);
|
||||||
}
|
}
|
||||||
.platform-msedge {
|
.platform-msedge {
|
||||||
@@ -3881,11 +3909,11 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-image: url(../images/platforms/msedge.svg);
|
background-image: url(../images/platforms/msedge.svg);
|
||||||
}
|
}
|
||||||
.platform-opera {
|
.platform-opera {
|
||||||
background-color: #ff1b2d;
|
background-color: #fa1e4e;
|
||||||
background-image: url(../images/platforms/opera.svg);
|
background-image: url(../images/platforms/opera.svg);
|
||||||
}
|
}
|
||||||
.platform-playstation {
|
.platform-playstation {
|
||||||
background-color: #034da2;
|
background-color: #003087;
|
||||||
background-image: url(../images/platforms/playstation.svg);
|
background-image: url(../images/platforms/playstation.svg);
|
||||||
}
|
}
|
||||||
.platform-plex {
|
.platform-plex {
|
||||||
@@ -3897,11 +3925,11 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-image: url(../images/platforms/plexamp.svg);
|
background-image: url(../images/platforms/plexamp.svg);
|
||||||
}
|
}
|
||||||
.platform-roku {
|
.platform-roku {
|
||||||
background-color: #6d3c97;
|
background-color: #673293;
|
||||||
background-image: url(../images/platforms/roku.svg);
|
background-image: url(../images/platforms/roku.svg);
|
||||||
}
|
}
|
||||||
.platform-safari {
|
.platform-safari {
|
||||||
background-color: #00a9ec;
|
background-color: #00d3f9;
|
||||||
background-image: url(../images/platforms/safari.svg);
|
background-image: url(../images/platforms/safari.svg);
|
||||||
}
|
}
|
||||||
.platform-samsung {
|
.platform-samsung {
|
||||||
@@ -3921,7 +3949,7 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-image: url(../images/platforms/wiiu.svg);
|
background-image: url(../images/platforms/wiiu.svg);
|
||||||
}
|
}
|
||||||
.platform-windows {
|
.platform-windows {
|
||||||
background-color: #2fc0f5;
|
background-color: #0078d7;
|
||||||
background-image: url(../images/platforms/windows.svg);
|
background-image: url(../images/platforms/windows.svg);
|
||||||
}
|
}
|
||||||
.platform-wp {
|
.platform-wp {
|
||||||
@@ -3937,55 +3965,55 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-image: url(../images/platforms/xbox.svg);
|
background-image: url(../images/platforms/xbox.svg);
|
||||||
}
|
}
|
||||||
.platform-android-rgba {
|
.platform-android-rgba {
|
||||||
background-color: rgba(164, 202, 57, 0.40);
|
background-color: rgba(61, 220, 132, 0.40);
|
||||||
}
|
}
|
||||||
.platform-atv-rgba {
|
.platform-atv-rgba {
|
||||||
background-color: rgba(133, 132, 135, 0.40);
|
background-color: rgba(162, 170, 173, 0.40);
|
||||||
}
|
}
|
||||||
.platform-chrome-rgba {
|
.platform-chrome-rgba {
|
||||||
background-color: rgba(237, 94, 80, 0.40);
|
background-color: rgba(219, 68, 55, 0.40);
|
||||||
}
|
}
|
||||||
.platform-chromecast-rgba {
|
.platform-chromecast-rgba {
|
||||||
background-color: rgba(16, 164, 232, 0.40);
|
background-color: rgba(66, 133, 244, 0.40);
|
||||||
}
|
}
|
||||||
.platform-default-rgba {
|
.platform-default-rgba {
|
||||||
background-color: rgba(229, 160, 13, 0.40);
|
background-color: rgba(229, 160, 13, 0.40);
|
||||||
}
|
}
|
||||||
.platform-dlna-rgba {
|
.platform-dlna-rgba {
|
||||||
background-color: rgba(12, 177, 75, 0.40);
|
background-color: rgba(75, 163, 47, 0.40);
|
||||||
}
|
}
|
||||||
.platform-firefox-rgba {
|
.platform-firefox-rgba {
|
||||||
background-color: rgba(230, 120, 23, 0.40);
|
background-color: rgba(255, 113, 57, 0.40);
|
||||||
}
|
}
|
||||||
.platform-gtv-rgba {
|
.platform-gtv-rgba {
|
||||||
background-color: rgba(0, 139, 207, 0.40);
|
background-color: rgba(0, 139, 207, 0.40);
|
||||||
}
|
}
|
||||||
.platform-ie-rgba {
|
.platform-ie-rgba {
|
||||||
background-color: rgba(0, 89, 158, 0.40);
|
background-color: rgba(24, 188, 239, 0.40);
|
||||||
}
|
}
|
||||||
.platform-ios-rgba {
|
.platform-ios-rgba {
|
||||||
background-color: rgba(133, 132, 135, 0.40);
|
background-color: rgba(162, 170, 173, 0.40);
|
||||||
}
|
}
|
||||||
.platform-kodi-rgba {
|
.platform-kodi-rgba {
|
||||||
background-color: rgba(49, 175, 225, 0.40);
|
background-color: rgba(48, 170, 218, 0.40);
|
||||||
}
|
}
|
||||||
.platform-lg-rgba {
|
.platform-lg-rgba {
|
||||||
background-color: rgba(165, 0, 52, 0.40);
|
background-color: rgba(153, 0, 51, 0.40);
|
||||||
}
|
}
|
||||||
.platform-linux-rgba {
|
.platform-linux-rgba {
|
||||||
background-color: rgba(23, 147, 208, 0.40);
|
background-color: rgba(0, 153, 204, 0.40);
|
||||||
}
|
}
|
||||||
.platform-macos-rgba {
|
.platform-macos-rgba {
|
||||||
background-color: rgba(133, 132, 135, 0.40);
|
background-color: rgba(162, 170, 173, 0.40);
|
||||||
}
|
}
|
||||||
.platform-msedge-rgba {
|
.platform-msedge-rgba {
|
||||||
background-color: rgba(0, 120, 215, 0.40);
|
background-color: rgba(0, 120, 215, 0.40);
|
||||||
}
|
}
|
||||||
.platform-opera-rgba {
|
.platform-opera-rgba {
|
||||||
background-color: rgba(255, 27, 45, 0.40);
|
background-color: rgba(250, 30, 78, 0.40);
|
||||||
}
|
}
|
||||||
.platform-playstation-rgba {
|
.platform-playstation-rgba {
|
||||||
background-color: rgba(3, 77, 162, 0.40);
|
background-color: rgba(0, 48, 135, 0.40);
|
||||||
}
|
}
|
||||||
.platform-plex-rgba {
|
.platform-plex-rgba {
|
||||||
background-color: rgba(229, 160, 13, 0.40);
|
background-color: rgba(229, 160, 13, 0.40);
|
||||||
@@ -3994,10 +4022,10 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-color: rgba(229, 160, 13, 0.40);
|
background-color: rgba(229, 160, 13, 0.40);
|
||||||
}
|
}
|
||||||
.platform-roku-rgba {
|
.platform-roku-rgba {
|
||||||
background-color: rgba(109, 60, 151, 0.40);
|
background-color: rgba(103, 50, 147, 0.40);
|
||||||
}
|
}
|
||||||
.platform-safari-rgba {
|
.platform-safari-rgba {
|
||||||
background-color: rgba(0, 169, 236, 0.40);
|
background-color: rgba(0, 211, 249, 0.40);
|
||||||
}
|
}
|
||||||
.platform-samsung-rgba {
|
.platform-samsung-rgba {
|
||||||
background-color: rgba(3, 78, 162, 0.40);
|
background-color: rgba(3, 78, 162, 0.40);
|
||||||
@@ -4012,7 +4040,7 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-color: rgba(3, 169, 244, 0.40);
|
background-color: rgba(3, 169, 244, 0.40);
|
||||||
}
|
}
|
||||||
.platform-windows-rgba {
|
.platform-windows-rgba {
|
||||||
background-color: rgba(47, 192, 245, 0.40);
|
background-color: rgba(0, 120, 215, 0.40);
|
||||||
}
|
}
|
||||||
.platform-wp-rgba {
|
.platform-wp-rgba {
|
||||||
background-color: rgba(104, 33, 122, 0.40);
|
background-color: rgba(104, 33, 122, 0.40);
|
||||||
|
|||||||
@@ -410,6 +410,8 @@ DOCUMENTATION :: END
|
|||||||
<i class="fa fa-fw fa-spinner"></i>
|
<i class="fa fa-fw fa-spinner"></i>
|
||||||
% elif data['state'] == 'error':
|
% elif data['state'] == 'error':
|
||||||
<i class="fa fa-fw fa-exclamation-triangle"></i>
|
<i class="fa fa-fw fa-exclamation-triangle"></i>
|
||||||
|
% else:
|
||||||
|
<i class="fa fa-fw fa-question-circle"></i>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-title">
|
<div class="dashboard-activity-metadata-title">
|
||||||
|
|||||||
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 plexpy
|
||||||
|
from plexpy import exporter
|
||||||
|
from plexpy.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>
|
||||||
</div>
|
</div>
|
||||||
<div class='table-card-back'>
|
<div class="table-card-back">
|
||||||
<ul class="nav nav-pills" role="tablist" id="graph-tabs">
|
<ul class="nav nav-list 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 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 href="#tabs-2" aria-controls="tabs-2" data-toggle="tab" role="tab">Stream Info</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 href="#tabs-3" aria-controls="tabs-3" data-toggle="tab" role="tab">Play Totals</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>
|
</ul>
|
||||||
<div class="tab-content">
|
<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="row">
|
||||||
<div class="col-md-12">
|
<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>
|
<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>
|
||||||
</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="row">
|
||||||
<div class="col-md-12">
|
<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>
|
<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>
|
||||||
</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="row">
|
||||||
<div class="col-md-12">
|
<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>
|
<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>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%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/highcharts/js/highcharts.js"></script>
|
||||||
<script src="${http_root}js/jquery.dataTables.min.js"></script>
|
<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.min.js"></script>
|
||||||
@@ -341,14 +339,29 @@
|
|||||||
var yaxis = getLocalStorage('graph_type', 'plays');
|
var yaxis = getLocalStorage('graph_type', 'plays');
|
||||||
var current_day_range = getLocalStorage('graph_days', 30);
|
var current_day_range = getLocalStorage('graph_days', 30);
|
||||||
var current_month_range = getLocalStorage('graph_months', 12);
|
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).prop('checked', true);
|
||||||
$('#yaxis-' + yaxis).closest('label').addClass('active');
|
$('#yaxis-' + yaxis).closest('label').addClass('active');
|
||||||
$('#graph-days').val(current_day_range);
|
$('#graph-days').val(current_day_range);
|
||||||
$('#graph-months').val(current_month_range);
|
$('#graph-months').val(current_month_range);
|
||||||
$('#graph-tabs a[href="' + current_tab + '"]').closest('li').addClass('active');
|
$('#nav-' + current_tab.replace('#', '')).tab('show').trigger('show.bs.tab');
|
||||||
$(current_tab).addClass('active');
|
//$(current_tab).addClass('active');
|
||||||
|
|
||||||
|
|
||||||
$('.days').html(current_day_range);
|
$('.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) {
|
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) {
|
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
|
// Set initial state
|
||||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||||
|
|
||||||
// Tab1 opened
|
// 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();
|
e.preventDefault();
|
||||||
current_tab = $(this).attr('href');
|
current_tab = $(this).attr('href');
|
||||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||||
@@ -603,7 +616,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Tab2 opened
|
// 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();
|
e.preventDefault();
|
||||||
current_tab = $(this).attr('href');
|
current_tab = $(this).attr('href');
|
||||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||||
@@ -611,7 +624,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Tab3 opened
|
// 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();
|
e.preventDefault();
|
||||||
current_tab = $(this).attr('href');
|
current_tab = $(this).attr('href');
|
||||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||||
@@ -624,8 +637,8 @@
|
|||||||
forceMinMax($(this));
|
forceMinMax($(this));
|
||||||
current_day_range = $(this).val();
|
current_day_range = $(this).val();
|
||||||
setLocalStorage('graph_days', current_day_range);
|
setLocalStorage('graph_days', current_day_range);
|
||||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||||
$('.days').html(current_day_range);
|
$('.days').html(current_day_range);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -635,25 +648,25 @@
|
|||||||
forceMinMax($(this));
|
forceMinMax($(this));
|
||||||
current_month_range = $(this).val();
|
current_month_range = $(this).val();
|
||||||
setLocalStorage('graph_months', current_month_range);
|
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);
|
$('.months').html(current_month_range);
|
||||||
});
|
});
|
||||||
|
|
||||||
// User changed
|
// User changed
|
||||||
$('#graph-user').on('change', function() {
|
$('#graph-user').on('change', function() {
|
||||||
selected_user_id = $(this).val() || null;
|
selected_user_id = $(this).val() || null;
|
||||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Y-axis changed
|
// Y-axis changed
|
||||||
$('#yaxis-selection').on('change', function() {
|
$('#yaxis-selection').on('change', function() {
|
||||||
yaxis = $('input[name=yaxis-options]:checked', '#yaxis-selection').val();
|
yaxis = $('input[name=yaxis-options]:checked', '#yaxis-selection').val();
|
||||||
setLocalStorage('graph_type', yaxis);
|
setLocalStorage('graph_type', yaxis);
|
||||||
if (current_tab === '#tabs-1') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-2') { loadGraphsTab2(current_day_range, yaxis); }
|
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-3') { loadGraphsTab3(current_month_range, yaxis); }
|
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||||
});
|
});
|
||||||
|
|
||||||
function setGraphFormat(type) {
|
function setGraphFormat(type) {
|
||||||
|
|||||||
@@ -117,7 +117,6 @@
|
|||||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
<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.min.js"></script>
|
||||||
<script src="${http_root}js/dataTables.bootstrap.pagination.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 src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|||||||
|
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>
|
<browserconfig>
|
||||||
<msapplication>
|
<msapplication>
|
||||||
<tile>
|
<tile>
|
||||||
<square150x150logo src="mstile-150x150.png?v=2.0.5"/>
|
<square150x150logo src="mstile-150x150.png?v=2.6.0"/>
|
||||||
<TileColor>#282a2d</TileColor>
|
<TileColor>#282a2d</TileColor>
|
||||||
</tile>
|
</tile>
|
||||||
</msapplication>
|
</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": "../../",
|
"scope": "../../",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "android-chrome-192x192.png?v=2.0.5",
|
"src": "android-chrome-192x192.png?v=2.6.0",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "android-chrome-256x256.png?v=2.0.5",
|
"src": "android-chrome-256x256.png?v=2.6.0",
|
||||||
"sizes": "256x256",
|
"sizes": "256x256",
|
||||||
"type": "image/png"
|
"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 -->
|
<!-- Generated by IcoMoon.io -->
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
<title>android</title>
|
<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="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>
|
||||||
<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>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -24,17 +24,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="currentActivity">
|
<div id="currentActivity">
|
||||||
% if PLEX_SERVER_UP:
|
% if PLEX_SERVER_UP:
|
||||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
<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']:
|
% elif config['pms_is_cloud']:
|
||||||
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
|
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
|
||||||
% elif not config['first_run_complete']:
|
% 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>
|
<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:
|
% else:
|
||||||
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
|
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to the Plex server...</div>
|
||||||
% if _session['user_group'] == 'admin':
|
|
||||||
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +61,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="home-stats" class="home-platforms">
|
<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>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +80,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="library-stats" class="library-platforms">
|
<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>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,18 +128,13 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="recentlyAdded" style="margin-right: -15px;">
|
<div id="recentlyAdded" style="margin-right: -15px;">
|
||||||
% if PLEX_SERVER_UP:
|
% 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']:
|
% elif config['pms_is_cloud']:
|
||||||
<div class="text-muted">Plex Cloud server is sleeping.</div>
|
<div class="text-muted">Plex Cloud server is sleeping.</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="text-muted">There was an error communicating with your Plex Server.
|
<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>
|
||||||
% 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
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% endif
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
@@ -229,8 +220,6 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%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.scrollbar.min.js"></script>
|
||||||
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -260,8 +249,33 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
% 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;
|
||||||
|
|
||||||
|
var server_status;
|
||||||
|
server_status = setInterval(function() {
|
||||||
|
$.getJSON('server_status', function (data) {
|
||||||
|
if (data.connected === true) {
|
||||||
|
clearInterval(server_status);
|
||||||
|
$('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>');
|
||||||
|
$('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>');
|
||||||
|
activityConnected();
|
||||||
|
recentlyAddedConnected();
|
||||||
|
} else if (data.connected === false) {
|
||||||
|
clearInterval(server_status);
|
||||||
|
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
||||||
|
$('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
</script>
|
</script>
|
||||||
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
|
% if 'current_activity' in config['home_sections']:
|
||||||
<script>
|
<script>
|
||||||
var defaultHandler = {
|
var defaultHandler = {
|
||||||
get: function(target, name) {
|
get: function(target, name) {
|
||||||
@@ -298,13 +312,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(current_activity)) {
|
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();
|
$('#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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +558,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the progress bars, percent - 3 because of 3px padding-right
|
// 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 + '%');
|
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||||
if (s.live !== 1) {
|
if (s.live !== 1) {
|
||||||
var progress_bar = $('#progress-bar-' + key);
|
var progress_bar = $('#progress-bar-' + key);
|
||||||
@@ -626,6 +635,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function activityConnected() {
|
||||||
getCurrentActivity();
|
getCurrentActivity();
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
if (!(create_instances.length) && activity_ready) {
|
if (!(create_instances.length) && activity_ready) {
|
||||||
@@ -648,12 +658,13 @@
|
|||||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||||
var progress_percent = Math.floor(view_offset / stream_duration * 100);
|
var progress_percent = Math.floor(view_offset / stream_duration * 100);
|
||||||
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
|
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
|
||||||
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
|
$(this).css({width: progress_percent - 3 + '%'}).html(progress_percent + '%')
|
||||||
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
||||||
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
$('#currentActivity').on('click', '.external_ip-modal', function () {
|
$('#currentActivity').on('click', '.external_ip-modal', function () {
|
||||||
$.get('get_ip_address_details', {
|
$.get('get_ip_address_details', {
|
||||||
@@ -877,7 +888,7 @@
|
|||||||
getLibraryStats();
|
getLibraryStats();
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
% if 'recently_added' in config['home_sections'] and PLEX_SERVER_UP:
|
% if 'recently_added' in config['home_sections']:
|
||||||
<script>
|
<script>
|
||||||
function recentlyAdded(recently_added_count, recently_added_type) {
|
function recentlyAdded(recently_added_count, recently_added_type) {
|
||||||
showMsg("Loading recently added items...", true, false, 0);
|
showMsg("Loading recently added items...", true, false, 0);
|
||||||
@@ -905,7 +916,9 @@
|
|||||||
$('#recently-added-toggle-' + recently_added_type).closest('label').addClass('active');
|
$('#recently-added-toggle-' + recently_added_type).closest('label').addClass('active');
|
||||||
$('#recently-added-count').val(recently_added_count);
|
$('#recently-added-count').val(recently_added_count);
|
||||||
|
|
||||||
|
function recentlyAddedConnected() {
|
||||||
recentlyAdded(recently_added_count, recently_added_type);
|
recentlyAdded(recently_added_count, recently_added_type);
|
||||||
|
}
|
||||||
|
|
||||||
function highlightAddedScrollerButton() {
|
function highlightAddedScrollerButton() {
|
||||||
var scroller = $("#recently-added-row-scroller");
|
var scroller = $("#recently-added-row-scroller");
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ DOCUMENTATION :: END
|
|||||||
|
|
||||||
from plexpy import notifiers
|
from plexpy import notifiers
|
||||||
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
|
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
|
||||||
from plexpy.helpers import page, get_percent
|
from plexpy.helpers import page, get_percent, cast_to_int
|
||||||
|
|
||||||
# Get audio codec file
|
# Get audio codec file
|
||||||
def af(codec):
|
def af(codec):
|
||||||
@@ -84,8 +84,10 @@ DOCUMENTATION :: END
|
|||||||
%>
|
%>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
% if data['media_type'] not in ('photo_album', 'photo', 'playlist'):
|
||||||
<% fallback = 'art-live-full' if data['live'] else None %>
|
<% 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>
|
<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':
|
% 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>
|
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||||
% endif
|
% endif
|
||||||
@@ -150,6 +152,29 @@ DOCUMENTATION :: END
|
|||||||
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
<li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
|
||||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||||
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
|
<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
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,10 +183,13 @@ DOCUMENTATION :: END
|
|||||||
<div class="summary-content-title-wrapper">
|
<div class="summary-content-title-wrapper">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||||
% if data['media_type'] == 'track':
|
<% legacy = '&legacy=1' if data['media_type'] in ('photo_album', 'photo', 'clip') else '' %>
|
||||||
<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">
|
% 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']:
|
% 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
|
% endif
|
||||||
% if data['live']:
|
% if data['live']:
|
||||||
<div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live')});">
|
<div class="summary-poster-face" 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':
|
% if _session['user_group'] == 'admin':
|
||||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||||
% endif
|
% endif
|
||||||
% 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-track" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 500, fallback='cover')});">
|
||||||
<div class="summary-poster-face-overlay">
|
<div class="summary-poster-face-overlay">
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||||
@@ -214,7 +245,7 @@ DOCUMENTATION :: END
|
|||||||
<h3 class="hidden-xs">S${data['parent_media_index']} · E${data['media_index']}</h3>
|
<h3 class="hidden-xs">S${data['parent_media_index']} · E${data['media_index']}</h3>
|
||||||
% endif
|
% endif
|
||||||
% 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>
|
<h1> </h1><h1>${data['title']}</h1>
|
||||||
% elif data['media_type'] == 'season':
|
% elif data['media_type'] == 'season':
|
||||||
<h1> </h1><h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
|
<h1> </h1><h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
|
||||||
@@ -230,26 +261,30 @@ DOCUMENTATION :: END
|
|||||||
<h1><a href="${page('info', data['grandparent_rating_key'])}">${data['original_title'] or data['grandparent_title']}</a></h1>
|
<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>
|
<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>
|
<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
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-content-wrapper">
|
<div class="summary-content-wrapper">
|
||||||
<div class="col-md-9">
|
<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;">
|
padding_height = ''
|
||||||
% elif data['media_type'] in ('show', 'season', 'collection'):
|
if data['media_type'] == 'movie' or data['live']:
|
||||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
|
padding_height = 'height: 305px;'
|
||||||
% elif data['media_type'] == 'episode':
|
elif data['media_type'] in ('show', 'season', 'collection'):
|
||||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;">
|
padding_height = 'height: 270px;'
|
||||||
% elif data['media_type'] == 'artist' or data['media_type'] == 'album':
|
elif data['media_type'] == 'episode':
|
||||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;">
|
padding_height = 'height: 70px;'
|
||||||
% elif data['media_type'] == 'track':
|
elif data['media_type'] in ('artist', 'album', 'playlist', 'photo_album', 'photo'):
|
||||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 180px;">
|
padding_height = 'height: 150px;'
|
||||||
% else:
|
elif data['media_type'] in ('track', 'clip'):
|
||||||
<div class="summary-content-padding hidden-xs hidden-sm">
|
padding_height = 'height: 180px;'
|
||||||
% endif
|
%>
|
||||||
% if data['media_type'] in ('movie', 'episode', 'track'):
|
<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">
|
<div class="summary-content-media-info-wrapper">
|
||||||
% if data['media_type'] != 'track' and media_info['video_codec']:
|
% 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" />
|
<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>
|
||||||
<div class="summary-content">
|
<div class="summary-content">
|
||||||
<div class="summary-content-details-wrapper">
|
<div class="summary-content-details-wrapper">
|
||||||
% if data['rating']:
|
<% rating = data['rating'] or data['audience_rating'] %>
|
||||||
% if data['rating_image']:
|
% if rating:
|
||||||
% if data['rating_image'].startswith('imdb://'):
|
% if data['audience_rating_image']:
|
||||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
% if data['audience_rating_image'].startswith('imdb://'):
|
||||||
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
|
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||||
|
<span class="rating-image rating-imdb"><strong>${rating}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% if data['rating_image'].startswith('themoviedb://'):
|
% if data['audience_rating_image'].startswith('themoviedb://'):
|
||||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||||
<span class="rating-image rating-themoviedb"><strong>${get_percent(data['rating'], 10)}%</strong></span>
|
<span class="rating-image rating-themoviedb"><strong>${get_percent(rating, 10)}%</strong></span>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% if data['audience_rating_image'].startswith('rottentomatoes://'):
|
% if data['audience_rating_image'].startswith('rottentomatoes://'):
|
||||||
@@ -291,11 +327,24 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% else:
|
% else:
|
||||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||||
<i class="star-icon fa fa-star"></i> <strong>${get_percent(data['rating'], 10)}%</strong>
|
<i class="star-icon fa fa-star"></i> <strong>${get_percent(rating, 10)}%</strong>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% 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">
|
<div class="summary-content-details-tag">
|
||||||
% if data['directors']:
|
% if data['directors']:
|
||||||
Directed by <strong> ${data['directors'][0]}</strong>
|
Directed by <strong> ${data['directors'][0]}</strong>
|
||||||
@@ -315,6 +364,8 @@ DOCUMENTATION :: END
|
|||||||
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
||||||
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
|
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||||
Released <strong> ${data['year']}</strong>
|
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':
|
% elif data['media_type'] == 'collection':
|
||||||
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
|
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
|
||||||
% elif data['year']:
|
% elif data['year']:
|
||||||
@@ -323,7 +374,7 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
<div class="summary-content-details-tag">
|
<div class="summary-content-details-tag">
|
||||||
% if data['duration']:
|
% if data['duration']:
|
||||||
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
|
Runtime <strong> <span id="runtime">${data['duration']}</span></strong>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-content-details-tag">
|
<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 id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading track list...</div>
|
||||||
</div>
|
</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':
|
% elif data['media_type'] == 'collection':
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-header">
|
<div class="table-card-header">
|
||||||
@@ -447,30 +509,36 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<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>
|
</div>
|
||||||
<div id="collection-related-list-container" style="display: none;">
|
<div id="collection-related-list-container" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% elif data['media_type'] == 'playlist':
|
||||||
% if data['media_type'] != 'collection':
|
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-header">
|
<div class="table-card-header">
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
% if data['media_type'] in ('artist', 'album', 'track'):
|
<span>${MEDIA_TYPE_HEADERS[data['playlist_type']]} List for <strong>${data['title']}</strong></span>
|
||||||
<span>Play History for <strong>${data['title']}</strong></span>
|
</div>
|
||||||
% else:
|
</div>
|
||||||
<span>Watch History for <strong>${data['title']}</strong></span>
|
<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
|
% endif
|
||||||
</div>
|
<%
|
||||||
|
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">
|
<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':
|
% if source == 'history':
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="update_metadata?rating_key=${data['rating_key']}&update=True" class="btn btn-danger btn-edit" id="fix-metadata">
|
<a href="update_metadata?rating_key=${data['rating_key']}&update=True" class="btn btn-danger btn-edit" id="fix-metadata">
|
||||||
@@ -510,11 +578,37 @@ DOCUMENTATION :: END
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
</div>
|
||||||
|
</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
|
% endif
|
||||||
<div class="btn-group">
|
<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>
|
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group colvis-button-bar"></div>
|
<div class="btn-group colvis-button-bar" id="button-bar-history"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
@@ -540,7 +634,61 @@ DOCUMENTATION :: END
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
% endif
|
% 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -629,6 +777,8 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
|
||||||
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
@@ -636,13 +786,14 @@ DOCUMENTATION :: END
|
|||||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
<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.min.js"></script>
|
||||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
|
||||||
|
|
||||||
% if metadata:
|
% if metadata:
|
||||||
<%
|
<%
|
||||||
data = defaultdict(None, **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/history_table.js${cache_param}"></script>
|
||||||
|
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
|
||||||
% if data['live']:
|
% if data['live']:
|
||||||
<script>
|
<script>
|
||||||
function get_history() {
|
function get_history() {
|
||||||
@@ -653,7 +804,7 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
guid: "${data['guid']}",
|
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 {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
grandparent_rating_key: "${data['rating_key']}",
|
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 {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
parent_rating_key: "${data['rating_key']}",
|
parent_rating_key: "${data['rating_key']}",
|
||||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
user_id: "${history_user_id}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -701,22 +852,157 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
rating_key: "${data['rating_key']}",
|
rating_key: "${data['rating_key']}",
|
||||||
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
user_id: "${history_user_id}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
% if data['media_type'] != 'collection':
|
% if data['media_type'] in ('movie', 'show', 'season', 'episode', 'artist', 'album', 'track'):
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
function loadHistoryTable() {
|
||||||
get_history();
|
get_history();
|
||||||
history_table = $('#history_table-RK-${data["rating_key"]}').DataTable(history_table_options);
|
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] });
|
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);
|
clearSearchButton('history_table-RK-${data["rating_key"]}', history_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
loadHistoryTable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-history-list").click(function () {
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
% 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']}",
|
||||||
|
media_type: "${data['media_type']}"
|
||||||
|
},
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#children-list").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
% if data['media_type'] == 'collection':
|
||||||
|
<script>
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_item_children_related',
|
||||||
|
type: 'GET',
|
||||||
|
async: true,
|
||||||
|
data: {
|
||||||
|
rating_key: "${data['rating_key']}",
|
||||||
|
title: "${data['title']}"
|
||||||
|
},
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#collection-related-list-container").html(xhr.responseText).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
<script>
|
||||||
|
$(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('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]',
|
||||||
|
html: true,
|
||||||
|
container: 'body',
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'right',
|
||||||
|
template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
||||||
|
content: function () {
|
||||||
|
return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% if _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').on('click', function() {
|
||||||
$('#row-edit-mode-alert').fadeIn(200);
|
$('#row-edit-mode-alert').fadeIn(200);
|
||||||
@@ -753,11 +1039,6 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
$("#refresh-history-list").click(function () {
|
|
||||||
history_table.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send recently added notification
|
// Send recently added notification
|
||||||
$('#send-recently-added-notification').on('click', function () {
|
$('#send-recently-added-notification').on('click', function () {
|
||||||
@@ -782,57 +1063,12 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
% endif
|
|
||||||
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
|
|
||||||
<script>
|
|
||||||
$.ajax({
|
|
||||||
url: 'get_item_children',
|
|
||||||
type: 'GET',
|
|
||||||
async: true,
|
|
||||||
data: { rating_key : "${data['rating_key']}" },
|
|
||||||
complete: function(xhr, status) {
|
|
||||||
$("#children-list").html(xhr.responseText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
% endif
|
|
||||||
% if data['media_type'] == 'collection':
|
|
||||||
<script>
|
|
||||||
$.ajax({
|
|
||||||
url: 'get_item_children_related',
|
|
||||||
type: 'GET',
|
|
||||||
async: true,
|
|
||||||
data: {
|
|
||||||
rating_key : "${data['rating_key']}",
|
|
||||||
title: "${data['title']}"
|
|
||||||
},
|
|
||||||
complete: function(xhr, status) {
|
|
||||||
$("#collection-related-list-container").html(xhr.responseText).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
% endif
|
|
||||||
<script>
|
|
||||||
$('.metadata-xml').on('tripleclick', function () {
|
$('.metadata-xml').on('tripleclick', function () {
|
||||||
openPlexXML("/library/metadata/${data['rating_key']}");
|
openPlexXML("/library/metadata/${data['rating_key']}");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
|
||||||
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
|
||||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
|
||||||
$('#channel-icon').popover({
|
|
||||||
selector: '[data-toggle=popover]',
|
|
||||||
html: true,
|
|
||||||
container: 'body',
|
|
||||||
trigger: 'hover',
|
|
||||||
placement: 'right',
|
|
||||||
template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
|
|
||||||
content: function () {
|
|
||||||
return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
% endif
|
||||||
% if data.get('poster_url'):
|
% if data.get('poster_url'):
|
||||||
<script>
|
<script>
|
||||||
$('#hosted-poster').popover({
|
$('#hosted-poster').popover({
|
||||||
|
|||||||
@@ -28,14 +28,15 @@ DOCUMENTATION :: END
|
|||||||
|
|
||||||
% if data != None:
|
% if data != None:
|
||||||
<%
|
<%
|
||||||
from plexpy.helpers import page
|
from plexpy.helpers import cast_to_int, page
|
||||||
%>
|
%>
|
||||||
% if data['children_count'] > 0:
|
% if data['children_count'] > 0:
|
||||||
<div class="item-children-wrapper">
|
<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']:
|
% for child in data['children_list']:
|
||||||
% if child['rating_key']:
|
% 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">
|
<li class="item-children-list-item">
|
||||||
% else:
|
% else:
|
||||||
<li>
|
<li>
|
||||||
@@ -123,37 +124,144 @@ DOCUMENTATION :: END
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
% elif data['children_type'] == 'track':
|
% elif data['children_type'] == 'track':
|
||||||
% if loop.index % 2 == 0:
|
<% e = 'even' if loop.index % 2 == 0 else 'odd' %>
|
||||||
<div class="item-children-list-item-even">
|
<div class="item-children-list-item-${e}">
|
||||||
<span class="item-children-list-item-index"> ${child['media_index']}</span>
|
<span class="item-children-list-item-index">${child['media_index']}</span>
|
||||||
<span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
|
<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']:
|
% if child['original_title']:
|
||||||
<span class="text-muted"> - ${child['original_title']}</span>
|
<span class="text-muted"> - ${child['original_title']}</span>
|
||||||
% endif
|
% endif
|
||||||
</span>
|
</span>
|
||||||
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
<% 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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
% 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:
|
% else:
|
||||||
<div class="item-children-list-item-odd">
|
<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>
|
||||||
<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
|
% 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>
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
% if child['duration']:
|
||||||
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||||
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
<% 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>
|
</span>
|
||||||
</div>
|
|
||||||
% endif
|
% 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>
|
</li>
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
|||||||
8
data/interfaces/default/js/bootstrap.min.js
vendored
@@ -790,6 +790,9 @@ ColVis.prototype = {
|
|||||||
oStyle.top = oPos.top+"px";
|
oStyle.top = oPos.top+"px";
|
||||||
oStyle.left = iDivX+"px";
|
oStyle.left = iDivX+"px";
|
||||||
|
|
||||||
|
var iDocWidth = $(document).width();
|
||||||
|
var iDocHeight = $(document).height();
|
||||||
|
|
||||||
document.body.appendChild( nBackground );
|
document.body.appendChild( nBackground );
|
||||||
document.body.appendChild( nHidden );
|
document.body.appendChild( nHidden );
|
||||||
document.body.appendChild( this.dom.catcher );
|
document.body.appendChild( this.dom.catcher );
|
||||||
@@ -819,12 +822,17 @@ ColVis.prototype = {
|
|||||||
|
|
||||||
var iDivWidth = $(nHidden).outerWidth();
|
var iDivWidth = $(nHidden).outerWidth();
|
||||||
var iDivHeight = $(nHidden).outerHeight();
|
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 )
|
if ( iLeft + iDivWidth > iDocWidth )
|
||||||
{
|
{
|
||||||
nHidden.style.left = (iDocWidth-iDivWidth)+"px";
|
nHidden.style.left = (iDocWidth-iDivWidth)+"px";
|
||||||
}
|
}
|
||||||
|
if ( iDivY + iDivHeight > iDocHeight )
|
||||||
|
{
|
||||||
|
nHidden.style.top = (oPos.top - iDivHeight - iDivMarginTop - iDivMarginBottom)+"px";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.s.hidden = false;
|
this.s.hidden = false;
|
||||||
@@ -846,7 +854,8 @@ ColVis.prototype = {
|
|||||||
this.s.hidden = true;
|
this.s.hidden = true;
|
||||||
|
|
||||||
$(this.dom.collection).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {
|
$(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) {
|
$(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 () {
|
String.prototype.toProperCase = function () {
|
||||||
return this.replace(/\w\S*/g, function (txt) {
|
return this.replace(/\w\S*/g, function (txt) {
|
||||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
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) {
|
function millisecondsToMinutes(ms, roundToMinute) {
|
||||||
if (ms > 0) {
|
if (ms > 0) {
|
||||||
var minutes = Math.floor(ms / 60000);
|
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
|
// Our countdown plugin takes a callback, a duration, and an optional message
|
||||||
$.fn.countdown = function (callback, duration, message) {
|
$.fn.countdown = function (callback, duration, message) {
|
||||||
// If no message is provided, we use an empty string
|
// 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) {
|
function setLocalStorage(key, value, path) {
|
||||||
|
var key_path = key;
|
||||||
if (path !== false) {
|
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) {
|
function getLocalStorage(key, default_value, path) {
|
||||||
|
var key_path = key;
|
||||||
if (path !== false) {
|
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) {
|
if (value !== null) {
|
||||||
return value
|
return value
|
||||||
} else if (default_value !== undefined) {
|
} else if (default_value !== undefined) {
|
||||||
@@ -803,3 +853,16 @@ function user_page(user_id, user) {
|
|||||||
|
|
||||||
return params;
|
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,15 +55,17 @@ history_table_options = {
|
|||||||
if (rowData['state'] !== null) {
|
if (rowData['state'] !== null) {
|
||||||
var state = '';
|
var state = '';
|
||||||
if (rowData['state'] === 'playing') {
|
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') {
|
} 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') {
|
} 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') {
|
} else if (rowData['state'] === 'error') {
|
||||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Playback Error"><i class="fa fa-exclamation-triangle fa-fw"></i></span>';
|
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') {
|
} 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>');
|
$(td).html('<div><div style="float: left;">' + state + ' ' + date + '</div></div>');
|
||||||
} else if (rowData['group_count'] > 1) {
|
} else if (rowData['group_count'] > 1) {
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ libraries_list_table_options = {
|
|||||||
"data": "duration",
|
"data": "duration",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== null && cellData !== '') {
|
if (cellData !== null && cellData !== '') {
|
||||||
$(td).html(humanTimeClean(cellData));
|
$(td).html(humanDuration(cellData, 'dhm', 's'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
|
|||||||
@@ -107,15 +107,15 @@ media_info_table_options = {
|
|||||||
} else if (rowData['media_type'] === 'photo_album') {
|
} 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>';
|
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>';
|
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') {
|
} 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>';
|
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>';
|
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') {
|
} 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>';
|
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>';
|
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 {
|
} else {
|
||||||
$(td).html(cellData);
|
$(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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -212,7 +212,7 @@ users_list_table_options = {
|
|||||||
"data": "duration",
|
"data": "duration",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== null && cellData !== '') {
|
if (cellData !== null && cellData !== '') {
|
||||||
$(td).html(humanTimeClean(cellData));
|
$(td).html(humanDuration(cellData, 'dhm', 's'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
|
|||||||
@@ -79,7 +79,6 @@
|
|||||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
<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.min.js"></script>
|
||||||
<script src="${http_root}js/dataTables.bootstrap.pagination.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 src="${http_root}js/tables/libraries.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|||||||
@@ -87,12 +87,19 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-nav">
|
<div class="user-info-nav">
|
||||||
<ul class="user-info-nav" role="tablist">
|
<ul class="nav nav-list nav-pills" role="tablist">
|
||||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
<li class="active"><a id="nav-tabs-profile" 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="nav-tabs-history" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||||
% if _session['user_group'] == 'admin':
|
|
||||||
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
% 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
|
||||||
% endif
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
@@ -242,23 +249,22 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
|
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
% if config['get_file_sizes'] and data['section_id'] in config['get_file_sizes_hold']['section_ids']:
|
% 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;">
|
<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.
|
<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 />
|
<br />
|
||||||
You may leave this page and check back later.
|
You may leave this page and check back later.
|
||||||
</div>
|
</div>
|
||||||
|
% endif
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span>
|
<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>
|
<span class="set-username">${data['section_name']}</span>
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
@@ -305,6 +311,157 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,8 +492,7 @@ DOCUMENTATION :: END
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modalIncludes()">
|
<%def name="modalIncludes()">
|
||||||
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog"
|
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="edit-library-modal">
|
||||||
aria-labelledby="edit-library-modal">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||||
</div>
|
</div>
|
||||||
@@ -360,6 +516,8 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
|
||||||
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
@@ -369,6 +527,9 @@ DOCUMENTATION :: END
|
|||||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||||
% if data:
|
% if data:
|
||||||
<% from plexpy.common import LIVE_TV_SECTION_ID %>
|
<% from plexpy.common import LIVE_TV_SECTION_ID %>
|
||||||
|
<%
|
||||||
|
history_user_id = '' if _session['user_group'] == 'admin' else _session['user_id']
|
||||||
|
%>
|
||||||
<script>
|
<script>
|
||||||
% if str(data['section_id']).isdigit():
|
% if str(data['section_id']).isdigit():
|
||||||
var section_id = ${data['section_id']};
|
var section_id = ${data['section_id']};
|
||||||
@@ -384,14 +545,18 @@ DOCUMENTATION :: END
|
|||||||
var get_file_sizes = null;
|
var get_file_sizes = null;
|
||||||
% endif
|
% endif
|
||||||
</script>
|
</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/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/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>
|
<script>
|
||||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".inactive-library-tooltip").tooltip();
|
||||||
|
|
||||||
function loadHistoryTable() {
|
function loadHistoryTable() {
|
||||||
// Build watch history table
|
// Build watch history table
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
@@ -401,7 +566,7 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
section_id: section_id,
|
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);
|
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') {
|
if (typeof(history_table) === 'undefined') {
|
||||||
loadHistoryTable();
|
loadHistoryTable();
|
||||||
}
|
}
|
||||||
@@ -422,97 +587,70 @@ DOCUMENTATION :: END
|
|||||||
$("#refresh-history-list").click(function () {
|
$("#refresh-history-list").click(function () {
|
||||||
history_table.draw();
|
history_table.draw();
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
$(".inactive-library-tooltip").tooltip();
|
% if data['section_type'] != 'artist':
|
||||||
|
<script>
|
||||||
% if _session['user_group'] == 'admin':
|
function loadCollectionsTable() {
|
||||||
function loadMediaInfoTable() {
|
// Build collections table
|
||||||
// Build media info table
|
collections_table_options.ajax = {
|
||||||
media_info_table_options.ajax = {
|
url: 'get_collections_list',
|
||||||
url: 'get_library_media_info',
|
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
section_id: section_id,
|
section_id: section_id
|
||||||
refresh: refresh_table
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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' });
|
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-media-info');
|
$(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() {
|
$('#nav-tabs-collections').on('shown.bs.tab', function() {
|
||||||
if (typeof(media_info_table) === 'undefined') {
|
if (typeof(collections_table) === 'undefined') {
|
||||||
loadMediaInfoTable();
|
loadCollectionsTable();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#refresh-media-info-table").click(function () {
|
$("#refresh-collections-table").click(function () {
|
||||||
media_info_child_table = {};
|
collections_table.draw();
|
||||||
refresh_table = true;
|
|
||||||
refresh_child_tables = true;
|
|
||||||
media_info_table.draw();
|
|
||||||
refresh_table = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#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
|
||||||
|
<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);
|
||||||
|
|
||||||
|
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["section_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 recentlyWatched() {
|
function recentlyWatched() {
|
||||||
// Populate recently watched
|
// Populate recently watched
|
||||||
@@ -634,11 +772,11 @@ DOCUMENTATION :: END
|
|||||||
var hash = document.location.hash;
|
var hash = document.location.hash;
|
||||||
var prefix = "tab_";
|
var prefix = "tab_";
|
||||||
if (hash) {
|
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
|
// 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);
|
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -664,5 +802,143 @@ DOCUMENTATION :: END
|
|||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</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
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
@@ -11,28 +11,28 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" 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/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/opensans.min.css" 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.all.min.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Favicons -->
|
<!-- 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="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.0.5">
|
<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.0.5">
|
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||||
|
|
||||||
<!-- ICONS -->
|
<!-- ICONS -->
|
||||||
<!-- Android -->
|
<!-- 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">
|
<meta name="theme-color" content="#282a2d">
|
||||||
<!-- Apple -->
|
<!-- Apple -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
<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.0.5" color="#282a2d">
|
<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">
|
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||||
<!-- Microsoft -->
|
<!-- Microsoft -->
|
||||||
<meta name="application-name" content="Tautulli">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body style="margin: 0; overflow: auto;">
|
<body style="margin: 0; overflow: auto;">
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
</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/platform.min.js"></script>
|
<script src="${http_root}js/platform.min.js"></script>
|
||||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -159,16 +159,20 @@
|
|||||||
data: data,
|
data: data,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
statusCode: {
|
statusCode: {
|
||||||
200: function() {
|
200: function(xhr, status) {
|
||||||
window.location = "${redirect_uri or http_root}";
|
window.location = "${redirect_uri or http_root}";
|
||||||
},
|
},
|
||||||
401: function() {
|
401: function(xhr, status) {
|
||||||
if (plex) {
|
if (plex) {
|
||||||
$('#sign-in-alert').text('Invalid Plex Login.').show();
|
$('#sign-in-alert').text('Invalid Plex Login.').show();
|
||||||
} else {
|
} else {
|
||||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||||
$('#username').focus();
|
$('#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() {
|
complete: function() {
|
||||||
|
|||||||
@@ -208,7 +208,6 @@
|
|||||||
<script src="${http_root}js/jquery.dataTables.min.js"></script>
|
<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.min.js"></script>
|
||||||
<script src="${http_root}js/dataTables.bootstrap.pagination.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/logs.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/tables/plex_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>
|
<script src="${http_root}js/tables/notification_logs.js${cache_param}"></script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Tautulli - ${title}</title>
|
<title>Tautulli - ${title}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/opensans.min.css" 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.all.min.css" rel="stylesheet">
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
<span id="simple_cron_message">Set the schedule for the newsletter.</span>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Tautulli - ${title} | ${server_name}</title>
|
<title>Tautulli - ${title} | ${server_name}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="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.0.5">
|
<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.0.5">
|
<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">
|
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</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>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
var frame = $('<iframe></iframe>', {
|
var frame = $('<iframe></iframe>', {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%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>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -48,17 +46,17 @@
|
|||||||
<!-- Nav tabs -->
|
<!-- Nav tabs -->
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<ul class="nav-settings list-unstyled" role="tablist">
|
<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" 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 href="#tabs-general" aria-controls="tabs-general" role="tab" data-toggle="tab">General</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 href="#tabs-homepage" aria-controls="tabs-homepage" role="tab" data-toggle="tab">Homepage</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 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-web_interface" 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 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 href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</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 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-notification_agents" 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 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 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-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 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-import_backups" 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"><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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@@ -830,7 +828,7 @@
|
|||||||
<p class="help-block">
|
<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.
|
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 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox advanced-setting">
|
<div class="checkbox advanced-setting">
|
||||||
@@ -979,15 +977,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox advanced-setting">
|
<div class="checkbox advanced-setting">
|
||||||
<label>
|
<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>
|
</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>
|
||||||
<div class="checkbox advanced-setting">
|
<div class="checkbox advanced-setting">
|
||||||
<label>
|
<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
|
<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>
|
</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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notify_concurrent_threshold">User Concurrent Stream Threshold</label>
|
<label for="notify_concurrent_threshold">User Concurrent Stream Threshold</label>
|
||||||
@@ -999,6 +1003,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="help-block">The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.</p>
|
<p class="help-block">The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.</p>
|
||||||
</div>
|
</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">
|
<div class="form-group advanced-setting">
|
||||||
<label for="notify_concurrent_threshold">Continued Session Threshold</label>
|
<label for="notify_concurrent_threshold">Continued Session Threshold</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -1179,7 +1192,7 @@
|
|||||||
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
|
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block">
|
<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>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<div id="plexpy-notifiers-table">
|
<div id="plexpy-notifiers-table">
|
||||||
@@ -1241,7 +1254,7 @@
|
|||||||
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p class="help-block">
|
<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.
|
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1264,7 +1277,7 @@
|
|||||||
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p class="help-block">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -1457,6 +1470,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||||
|
|
||||||
@@ -1471,9 +1500,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Get the App</label>
|
<label>Get the App</label>
|
||||||
<p class="help-block">
|
<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">
|
<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>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1509,14 +1538,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
|
<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>
|
in the README document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Your post may be removed for failure to follow the guidelines.
|
Your post may be removed for failure to follow the guidelines.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1531,12 +1560,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
|
<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>
|
before asking for help!</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1986,8 +2015,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
<script src="${http_root}js/parsley.min.js"></script>
|
<script src="${http_root}js/parsley.min.js"></script>
|
||||||
<script src="${http_root}js/Sortable.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/jquery.inputaffix.min.js"></script>
|
||||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
|
||||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function getConfigurationTable() {
|
function getConfigurationTable() {
|
||||||
@@ -2139,9 +2167,9 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Javascript to enable link to tab
|
// Javascript to enable link to tab
|
||||||
var hash = document.location.hash;
|
var hash = document.location.hash;
|
||||||
var prefix = "tab_";
|
var prefix = "tabs_";
|
||||||
if (hash) {
|
if (hash) {
|
||||||
$('.nav-settings a[href='+hash.replace(prefix,"")+']').tab('show');
|
$('.nav-settings #nav-' + hash.replace('#' + prefix, "")).tab('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change hash for page-reload
|
// Change hash for page-reload
|
||||||
@@ -2166,6 +2194,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
function preSaveChecks(_callback) {
|
function preSaveChecks(_callback) {
|
||||||
verifyPMSWebURL();
|
verifyPMSWebURL();
|
||||||
|
setBaseURLSuffix(true);
|
||||||
if (serverChanged) {
|
if (serverChanged) {
|
||||||
verifyServer(_callback);
|
verifyServer(_callback);
|
||||||
} else if (typeof _callback === "function") {
|
} else if (typeof _callback === "function") {
|
||||||
@@ -2185,6 +2214,7 @@ $(document).ready(function() {
|
|||||||
getNewslettersTable();
|
getNewslettersTable();
|
||||||
getMobileDevicesTable();
|
getMobileDevicesTable();
|
||||||
loadUpdateDistros();
|
loadUpdateDistros();
|
||||||
|
setBaseURLSuffix();
|
||||||
settingsChanged = false;
|
settingsChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2307,6 +2337,12 @@ $(document).ready(function() {
|
|||||||
confirmAjaxCall(url, msg);
|
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 () {
|
$("#clear_logs").click(function () {
|
||||||
var msg = 'Are you sure you want to clear the Tautulli logs?';
|
var msg = 'Are you sure you want to clear the Tautulli logs?';
|
||||||
var url = 'delete_logs';
|
var url = 'delete_logs';
|
||||||
@@ -2890,10 +2926,19 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#http_base_url').change(function () {
|
$('#http_root').change(function() {
|
||||||
$(this).val($(this).val().replace(/\/*$/, ''));
|
setBaseURLSuffix();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setBaseURLSuffix(clear) {
|
||||||
|
if (clear){
|
||||||
|
$('#http_base_url').suffix("");
|
||||||
|
} else {
|
||||||
|
$('#http_base_url').suffix($('#http_root').val());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBaseURLSuffix();
|
||||||
|
|
||||||
function apiEnabled() {
|
function apiEnabled() {
|
||||||
var api_enabled = $('#api_enabled').prop('checked');
|
var api_enabled = $('#api_enabled').prop('checked');
|
||||||
$('#app_api_msg').toggle(!(api_enabled));
|
$('#app_api_msg').toggle(!(api_enabled));
|
||||||
@@ -3012,7 +3057,7 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function gotoSetting(tab, setting){
|
function gotoSetting(tab, setting){
|
||||||
$("a[href=#tabs-" + tab + "]").click();
|
$("#nav-tabs-" + tab).click();
|
||||||
if (setting) {
|
if (setting) {
|
||||||
_setting = '#' + setting;
|
_setting = '#' + setting;
|
||||||
if ($(_setting).closest('.advanced-setting').length && !$('#menu_link_show_advanced_settings').hasClass('active')) {
|
if ($(_setting).closest('.advanced-setting').length && !$('#menu_link_show_advanced_settings').hasClass('active')) {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<div class="btn-group">
|
<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>
|
||||||
<div class="btn-group">
|
<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>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-dark" id="popout-iframe-button"><i class="fa fa-external-link"></i> Pop Out Chat</button>
|
<button class="btn btn-dark" id="popout-iframe-button"><i class="fa fa-external-link"></i> Pop Out Chat</button>
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ DOCUMENTATION :: END
|
|||||||
<div class="summary-navbar">
|
<div class="summary-navbar">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="summary-navbar-list">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,12 +69,16 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-nav">
|
<div class="user-info-nav">
|
||||||
<ul class="user-info-nav" role="tablist">
|
<ul class="nav nav-list nav-pills" role="tablist">
|
||||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
<li class="active"><a id="nav-tabs-profile" 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="nav-tabs-history" 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="nav-tabs-playlists" href="#tabs-playlists" role="tab" data-toggle="tab">Playlists</a></li>
|
||||||
<li><a id="ip-tab-btn" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
|
% if _session['user_group'] == 'admin':
|
||||||
<li><a id="login-tab-btn" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,6 +216,99 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 role="tabpanel" class="tab-pane" id="tabs-synceditems">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -278,6 +377,7 @@ DOCUMENTATION :: END
|
|||||||
<div class="btn-group">
|
<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>
|
<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>
|
||||||
|
<div class="btn-group colvis-button-bar" id="button-bar-ip-address"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
@@ -393,6 +493,8 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="export-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="export-modal">
|
||||||
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
@@ -410,8 +512,9 @@ DOCUMENTATION :: END
|
|||||||
|
|
||||||
var username = '${data['username'].replace("'", "\\'")}';
|
var username = '${data['username'].replace("'", "\\'")}';
|
||||||
</script>
|
</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/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/user_ips.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/tables/sync_table.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>
|
<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();
|
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".inactive-user-tooltip").tooltip();
|
||||||
|
|
||||||
function loadHistoryTable(media_type) {
|
function loadHistoryTable(media_type) {
|
||||||
// Build watch history table
|
// Build watch history table
|
||||||
history_table_options.ajax = {
|
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() {
|
function loadSyncTable() {
|
||||||
// Build user sync table
|
// Build user sync table
|
||||||
sync_table_options.ajax = {
|
sync_table_options.ajax = {
|
||||||
@@ -466,6 +614,16 @@ DOCUMENTATION :: END
|
|||||||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
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() {
|
function loadIPAddressTable() {
|
||||||
// Build user IP table
|
// Build user IP table
|
||||||
user_ip_table_options.ajax = {
|
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);
|
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);
|
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() {
|
function loadLoginTable() {
|
||||||
// Build user login table
|
// Build user login table
|
||||||
login_log_table_options.ajax = {
|
login_log_table_options.ajax = {
|
||||||
@@ -504,52 +675,141 @@ DOCUMENTATION :: END
|
|||||||
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
$('#nav-tabs-tautullilogins').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() {
|
|
||||||
if (typeof(login_log_table) === 'undefined') {
|
if (typeof(login_log_table) === 'undefined') {
|
||||||
loadLoginTable(user_id);
|
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 () {
|
$("#refresh-login-list").click(function () {
|
||||||
login_log_table.draw();
|
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':
|
% 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();
|
||||||
|
});
|
||||||
|
|
||||||
$("#edit-user-tooltip").tooltip();
|
$("#edit-user-tooltip").tooltip();
|
||||||
|
|
||||||
// Load edit user modal
|
// 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').on('click', function() {
|
||||||
$('#row-edit-mode-alert').fadeIn(200);
|
$('#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>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
@@ -82,7 +82,6 @@
|
|||||||
<script src="${http_root}js/dataTables.colVis.js"></script>
|
<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.min.js"></script>
|
||||||
<script src="${http_root}js/dataTables.bootstrap.pagination.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 src="${http_root}js/tables/users.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" 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/bootstrap-wizard.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/selectize.bootstrap3.css" 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">
|
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Favicons -->
|
<!-- 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="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.0.5">
|
<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.0.5">
|
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||||
|
|
||||||
<!-- ICONS -->
|
<!-- ICONS -->
|
||||||
<!-- Android -->
|
<!-- 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">
|
<meta name="theme-color" content="#282a2d">
|
||||||
<!-- Apple -->
|
<!-- Apple -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
<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.0.5" color="#282a2d">
|
<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">
|
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||||
<!-- Microsoft -->
|
<!-- Microsoft -->
|
||||||
<meta name="application-name" content="Tautulli">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
</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/bootstrap.min.js"></script>
|
||||||
<script src="${http_root}js/selectize.min.js"></script>
|
<script src="${http_root}js/selectize.min.js"></script>
|
||||||
<script src="${http_root}js/platform.min.js"></script>
|
<script src="${http_root}js/platform.min.js"></script>
|
||||||
|
|||||||
@@ -521,7 +521,7 @@
|
|||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.apple-link a {
|
a[x-apple-data-detectors] {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
font-family: inherit !important;
|
font-family: inherit !important;
|
||||||
font-size: 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>
|
<span class="preheader" style="color: transparent;display: none;height: 0;max-height: 0;max-width: 0;opacity: 0;overflow: hidden;mso-hide: all;visibility: hidden;width: 0;">Tautulli Newsletter - ${subject}</span>
|
||||||
% if base_url and not preview:
|
% if base_url and not preview:
|
||||||
<div class="view-full" style="clear: both;color: #282A2D;font-size: 12px;margin-bottom: 10px;text-align: center;width: 100%;"> <!-- IGNORE SAVE -->
|
<div class="view-full" style="clear: both;color: #282A2D;font-size: 12px;margin-bottom: 10px;text-align: center;width: 100%;"> <!-- IGNORE SAVE -->
|
||||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" rel="noreferrer" style="text-decoration: underline;color: #282A2D;">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||||
</div> <!-- IGNORE SAVE -->
|
</div> <!-- IGNORE SAVE -->
|
||||||
% endif
|
% endif
|
||||||
<table border="0" cellpadding="3" cellspacing="0" class="main" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background: #282A2D;border-radius: 3px;color: #ffffff;">
|
<table border="0" cellpadding="3" cellspacing="0" class="main" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background: #282A2D;border-radius: 3px;color: #ffffff;">
|
||||||
@@ -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);">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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;">
|
<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>
|
</a>
|
||||||
</td>
|
</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%;">
|
<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>
|
<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;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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);">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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;">
|
<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>
|
</a>
|
||||||
</td>
|
</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%;">
|
<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>
|
<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;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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);">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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;">
|
<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>
|
</a>
|
||||||
</td>
|
</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%;">
|
<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>
|
<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;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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);">
|
<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>
|
<tr>
|
||||||
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
|
||||||
<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;">
|
<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>
|
</a>
|
||||||
</td>
|
</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%;">
|
<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>
|
<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;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -521,7 +521,7 @@
|
|||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.apple-link a {
|
a[x-apple-data-detectors] {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
font-family: inherit !important;
|
font-family: inherit !important;
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
@@ -546,7 +546,7 @@
|
|||||||
<span class="preheader">Tautulli Newsletter - ${subject}</span>
|
<span class="preheader">Tautulli Newsletter - ${subject}</span>
|
||||||
% if base_url and not preview:
|
% if base_url and not preview:
|
||||||
<div class="view-full"> <!-- IGNORE SAVE -->
|
<div class="view-full"> <!-- IGNORE SAVE -->
|
||||||
<a href="${base_url + uuid}" title="View full newsletter" target="_blank">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
<a href="${base_url + uuid}" title="View full newsletter" target="_blank" rel="noreferrer">Click here to view the full newsletter.</a> <!-- IGNORE SAVE -->
|
||||||
</div> <!-- IGNORE SAVE -->
|
</div> <!-- IGNORE SAVE -->
|
||||||
% endif
|
% endif
|
||||||
<table border="0" cellpadding="3" cellspacing="0" class="main">
|
<table border="0" cellpadding="3" cellspacing="0" class="main">
|
||||||
@@ -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']})">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']})">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -610,7 +610,7 @@
|
|||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="card-info-title nowrap">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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']})">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']})">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -739,7 +739,7 @@
|
|||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="card-info-title nowrap">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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']})">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']})">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -883,7 +883,7 @@
|
|||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="card-info-title nowrap">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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']})">
|
<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>
|
<tr>
|
||||||
<td>
|
<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">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -997,7 +997,7 @@
|
|||||||
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
<table border="0" cellpadding="0" cellspacing="0" class="card-info-container-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="card-info-title nowrap">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ Wants=network-online.target
|
|||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
|
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --nolaunch
|
||||||
GuessMainPID=no
|
|
||||||
Type=forking
|
|
||||||
User=tautulli
|
User=tautulli
|
||||||
Group=tautulli
|
Group=tautulli
|
||||||
Restart=on-abnormal
|
Restart=on-abnormal
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
__import__('pkg_resources').declare_namespace(__name__)
|
||||||
|
|||||||
979
lib/backports/csv.py
Normal file
@@ -0,0 +1,979 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""A port of Python 3's csv module to Python 2.
|
||||||
|
|
||||||
|
The API of the csv module in Python 2 is drastically different from
|
||||||
|
the csv module in Python 3. This is due, for the most part, to the
|
||||||
|
difference between str in Python 2 and Python 3.
|
||||||
|
|
||||||
|
The semantics of Python 3's version are more useful because they support
|
||||||
|
unicode natively, while Python 2's csv does not.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
__all__ = [ "QUOTE_MINIMAL", "QUOTE_ALL", "QUOTE_NONNUMERIC", "QUOTE_NONE",
|
||||||
|
"Error", "Dialect", "__doc__", "excel", "excel_tab",
|
||||||
|
"field_size_limit", "reader", "writer",
|
||||||
|
"register_dialect", "get_dialect", "list_dialects", "Sniffer",
|
||||||
|
"unregister_dialect", "__version__", "DictReader", "DictWriter" ]
|
||||||
|
|
||||||
|
import re
|
||||||
|
import numbers
|
||||||
|
from io import StringIO
|
||||||
|
from csv import (
|
||||||
|
QUOTE_MINIMAL, QUOTE_ALL, QUOTE_NONNUMERIC, QUOTE_NONE,
|
||||||
|
__version__, __doc__, Error, field_size_limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stuff needed from six
|
||||||
|
import sys
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
if PY3:
|
||||||
|
string_types = str
|
||||||
|
text_type = str
|
||||||
|
binary_type = bytes
|
||||||
|
unichr = chr
|
||||||
|
else:
|
||||||
|
string_types = basestring
|
||||||
|
text_type = unicode
|
||||||
|
binary_type = str
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteStrategy(object):
|
||||||
|
quoting = None
|
||||||
|
|
||||||
|
def __init__(self, dialect):
|
||||||
|
if self.quoting is not None:
|
||||||
|
assert dialect.quoting == self.quoting
|
||||||
|
self.dialect = dialect
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
escape_pattern_quoted = r'({quotechar})'.format(
|
||||||
|
quotechar=re.escape(self.dialect.quotechar or '"'))
|
||||||
|
escape_pattern_unquoted = r'([{specialchars}])'.format(
|
||||||
|
specialchars=re.escape(self.specialchars))
|
||||||
|
|
||||||
|
self.escape_re_quoted = re.compile(escape_pattern_quoted)
|
||||||
|
self.escape_re_unquoted = re.compile(escape_pattern_unquoted)
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Optional method for strategy-wide optimizations."""
|
||||||
|
|
||||||
|
def quoted(self, field=None, raw_field=None, only=None):
|
||||||
|
"""Determine whether this field should be quoted."""
|
||||||
|
raise NotImplementedError(
|
||||||
|
'quoted must be implemented by a subclass')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specialchars(self):
|
||||||
|
"""The special characters that need to be escaped."""
|
||||||
|
raise NotImplementedError(
|
||||||
|
'specialchars must be implemented by a subclass')
|
||||||
|
|
||||||
|
def escape_re(self, quoted=None):
|
||||||
|
if quoted:
|
||||||
|
return self.escape_re_quoted
|
||||||
|
return self.escape_re_unquoted
|
||||||
|
|
||||||
|
def escapechar(self, quoted=None):
|
||||||
|
if quoted and self.dialect.doublequote:
|
||||||
|
return self.dialect.quotechar
|
||||||
|
return self.dialect.escapechar
|
||||||
|
|
||||||
|
def prepare(self, raw_field, only=None):
|
||||||
|
field = text_type(raw_field if raw_field is not None else '')
|
||||||
|
quoted = self.quoted(field=field, raw_field=raw_field, only=only)
|
||||||
|
|
||||||
|
escape_re = self.escape_re(quoted=quoted)
|
||||||
|
escapechar = self.escapechar(quoted=quoted)
|
||||||
|
|
||||||
|
if escape_re.search(field):
|
||||||
|
escapechar = '\\\\' if escapechar == '\\' else escapechar
|
||||||
|
if not escapechar:
|
||||||
|
raise Error('No escapechar is set')
|
||||||
|
escape_replace = r'{escapechar}\1'.format(escapechar=escapechar)
|
||||||
|
field = escape_re.sub(escape_replace, field)
|
||||||
|
|
||||||
|
if quoted:
|
||||||
|
field = '{quotechar}{field}{quotechar}'.format(
|
||||||
|
quotechar=self.dialect.quotechar, field=field)
|
||||||
|
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteMinimalStrategy(QuoteStrategy):
|
||||||
|
quoting = QUOTE_MINIMAL
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.quoted_re = re.compile(r'[{specialchars}]'.format(
|
||||||
|
specialchars=re.escape(self.specialchars)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specialchars(self):
|
||||||
|
return (
|
||||||
|
self.dialect.lineterminator +
|
||||||
|
self.dialect.quotechar +
|
||||||
|
self.dialect.delimiter +
|
||||||
|
(self.dialect.escapechar or '')
|
||||||
|
)
|
||||||
|
|
||||||
|
def quoted(self, field, only, **kwargs):
|
||||||
|
if field == self.dialect.quotechar and not self.dialect.doublequote:
|
||||||
|
# If the only character in the field is the quotechar, and
|
||||||
|
# doublequote is false, then just escape without outer quotes.
|
||||||
|
return False
|
||||||
|
return field == '' and only or bool(self.quoted_re.search(field))
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteAllStrategy(QuoteStrategy):
|
||||||
|
quoting = QUOTE_ALL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specialchars(self):
|
||||||
|
return self.dialect.quotechar
|
||||||
|
|
||||||
|
def quoted(self, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteNonnumericStrategy(QuoteStrategy):
|
||||||
|
quoting = QUOTE_NONNUMERIC
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specialchars(self):
|
||||||
|
return (
|
||||||
|
self.dialect.lineterminator +
|
||||||
|
self.dialect.quotechar +
|
||||||
|
self.dialect.delimiter +
|
||||||
|
(self.dialect.escapechar or '')
|
||||||
|
)
|
||||||
|
|
||||||
|
def quoted(self, raw_field, **kwargs):
|
||||||
|
return not isinstance(raw_field, numbers.Number)
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteNoneStrategy(QuoteStrategy):
|
||||||
|
quoting = QUOTE_NONE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specialchars(self):
|
||||||
|
return (
|
||||||
|
self.dialect.lineterminator +
|
||||||
|
(self.dialect.quotechar or '') +
|
||||||
|
self.dialect.delimiter +
|
||||||
|
(self.dialect.escapechar or '')
|
||||||
|
)
|
||||||
|
|
||||||
|
def quoted(self, field, only, **kwargs):
|
||||||
|
if field == '' and only:
|
||||||
|
raise Error('single empty field record must be quoted')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class writer(object):
|
||||||
|
def __init__(self, fileobj, dialect='excel', **fmtparams):
|
||||||
|
if fileobj is None:
|
||||||
|
raise TypeError('fileobj must be file-like, not None')
|
||||||
|
|
||||||
|
self.fileobj = fileobj
|
||||||
|
|
||||||
|
if isinstance(dialect, text_type):
|
||||||
|
dialect = get_dialect(dialect)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dialect = Dialect.combine(dialect, fmtparams)
|
||||||
|
except Error as e:
|
||||||
|
raise TypeError(*e.args)
|
||||||
|
|
||||||
|
strategies = {
|
||||||
|
QUOTE_MINIMAL: QuoteMinimalStrategy,
|
||||||
|
QUOTE_ALL: QuoteAllStrategy,
|
||||||
|
QUOTE_NONNUMERIC: QuoteNonnumericStrategy,
|
||||||
|
QUOTE_NONE: QuoteNoneStrategy,
|
||||||
|
}
|
||||||
|
self.strategy = strategies[self.dialect.quoting](self.dialect)
|
||||||
|
|
||||||
|
def writerow(self, row):
|
||||||
|
if row is None:
|
||||||
|
raise Error('row must be an iterable')
|
||||||
|
|
||||||
|
row = list(row)
|
||||||
|
only = len(row) == 1
|
||||||
|
row = [self.strategy.prepare(field, only=only) for field in row]
|
||||||
|
|
||||||
|
line = self.dialect.delimiter.join(row) + self.dialect.lineterminator
|
||||||
|
return self.fileobj.write(line)
|
||||||
|
|
||||||
|
def writerows(self, rows):
|
||||||
|
for row in rows:
|
||||||
|
self.writerow(row)
|
||||||
|
|
||||||
|
|
||||||
|
START_RECORD = 0
|
||||||
|
START_FIELD = 1
|
||||||
|
ESCAPED_CHAR = 2
|
||||||
|
IN_FIELD = 3
|
||||||
|
IN_QUOTED_FIELD = 4
|
||||||
|
ESCAPE_IN_QUOTED_FIELD = 5
|
||||||
|
QUOTE_IN_QUOTED_FIELD = 6
|
||||||
|
EAT_CRNL = 7
|
||||||
|
AFTER_ESCAPED_CRNL = 8
|
||||||
|
|
||||||
|
|
||||||
|
class reader(object):
|
||||||
|
def __init__(self, fileobj, dialect='excel', **fmtparams):
|
||||||
|
self.input_iter = iter(fileobj)
|
||||||
|
|
||||||
|
if isinstance(dialect, text_type):
|
||||||
|
dialect = get_dialect(dialect)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dialect = Dialect.combine(dialect, fmtparams)
|
||||||
|
except Error as e:
|
||||||
|
raise TypeError(*e.args)
|
||||||
|
|
||||||
|
self.fields = None
|
||||||
|
self.field = None
|
||||||
|
self.line_num = 0
|
||||||
|
|
||||||
|
def parse_reset(self):
|
||||||
|
self.fields = []
|
||||||
|
self.field = []
|
||||||
|
self.state = START_RECORD
|
||||||
|
self.numeric_field = False
|
||||||
|
|
||||||
|
def parse_save_field(self):
|
||||||
|
field = ''.join(self.field)
|
||||||
|
self.field = []
|
||||||
|
if self.numeric_field:
|
||||||
|
field = float(field)
|
||||||
|
self.numeric_field = False
|
||||||
|
self.fields.append(field)
|
||||||
|
|
||||||
|
def parse_add_char(self, c):
|
||||||
|
if len(self.field) >= field_size_limit():
|
||||||
|
raise Error('field size limit exceeded')
|
||||||
|
self.field.append(c)
|
||||||
|
|
||||||
|
def parse_process_char(self, c):
|
||||||
|
switch = {
|
||||||
|
START_RECORD: self._parse_start_record,
|
||||||
|
START_FIELD: self._parse_start_field,
|
||||||
|
ESCAPED_CHAR: self._parse_escaped_char,
|
||||||
|
AFTER_ESCAPED_CRNL: self._parse_after_escaped_crnl,
|
||||||
|
IN_FIELD: self._parse_in_field,
|
||||||
|
IN_QUOTED_FIELD: self._parse_in_quoted_field,
|
||||||
|
ESCAPE_IN_QUOTED_FIELD: self._parse_escape_in_quoted_field,
|
||||||
|
QUOTE_IN_QUOTED_FIELD: self._parse_quote_in_quoted_field,
|
||||||
|
EAT_CRNL: self._parse_eat_crnl,
|
||||||
|
}
|
||||||
|
return switch[self.state](c)
|
||||||
|
|
||||||
|
def _parse_start_record(self, c):
|
||||||
|
if c == '\0':
|
||||||
|
return
|
||||||
|
elif c == '\n' or c == '\r':
|
||||||
|
self.state = EAT_CRNL
|
||||||
|
return
|
||||||
|
|
||||||
|
self.state = START_FIELD
|
||||||
|
return self._parse_start_field(c)
|
||||||
|
|
||||||
|
def _parse_start_field(self, c):
|
||||||
|
if c == '\n' or c == '\r' or c == '\0':
|
||||||
|
self.parse_save_field()
|
||||||
|
self.state = START_RECORD if c == '\0' else EAT_CRNL
|
||||||
|
elif (c == self.dialect.quotechar and
|
||||||
|
self.dialect.quoting != QUOTE_NONE):
|
||||||
|
self.state = IN_QUOTED_FIELD
|
||||||
|
elif c == self.dialect.escapechar:
|
||||||
|
self.state = ESCAPED_CHAR
|
||||||
|
elif c == ' ' and self.dialect.skipinitialspace:
|
||||||
|
pass # Ignore space at start of field
|
||||||
|
elif c == self.dialect.delimiter:
|
||||||
|
# Save empty field
|
||||||
|
self.parse_save_field()
|
||||||
|
else:
|
||||||
|
# Begin new unquoted field
|
||||||
|
if self.dialect.quoting == QUOTE_NONNUMERIC:
|
||||||
|
self.numeric_field = True
|
||||||
|
self.parse_add_char(c)
|
||||||
|
self.state = IN_FIELD
|
||||||
|
|
||||||
|
def _parse_escaped_char(self, c):
|
||||||
|
if c == '\n' or c == '\r':
|
||||||
|
self.parse_add_char(c)
|
||||||
|
self.state = AFTER_ESCAPED_CRNL
|
||||||
|
return
|
||||||
|
if c == '\0':
|
||||||
|
c = '\n'
|
||||||
|
self.parse_add_char(c)
|
||||||
|
self.state = IN_FIELD
|
||||||
|
|
||||||
|
def _parse_after_escaped_crnl(self, c):
|
||||||
|
if c == '\0':
|
||||||
|
return
|
||||||
|
return self._parse_in_field(c)
|
||||||
|
|
||||||
|
def _parse_in_field(self, c):
|
||||||
|
# In unquoted field
|
||||||
|
if c == '\n' or c == '\r' or c == '\0':
|
||||||
|
# End of line - return [fields]
|
||||||
|
self.parse_save_field()
|
||||||
|
self.state = START_RECORD if c == '\0' else EAT_CRNL
|
||||||
|
elif c == self.dialect.escapechar:
|
||||||
|
self.state = ESCAPED_CHAR
|
||||||
|
elif c == self.dialect.delimiter:
|
||||||
|
self.parse_save_field()
|
||||||
|
self.state = START_FIELD
|
||||||
|
else:
|
||||||
|
# Normal character - save in field
|
||||||
|
self.parse_add_char(c)
|
||||||
|
|
||||||
|
def _parse_in_quoted_field(self, c):
|
||||||
|
if c == '\0':
|
||||||
|
pass
|
||||||
|
elif c == self.dialect.escapechar:
|
||||||
|
self.state = ESCAPE_IN_QUOTED_FIELD
|
||||||
|
elif (c == self.dialect.quotechar and
|
||||||
|
self.dialect.quoting != QUOTE_NONE):
|
||||||
|
if self.dialect.doublequote:
|
||||||
|
self.state = QUOTE_IN_QUOTED_FIELD
|
||||||
|
else:
|
||||||
|
self.state = IN_FIELD
|
||||||
|
else:
|
||||||
|
self.parse_add_char(c)
|
||||||
|
|
||||||
|
def _parse_escape_in_quoted_field(self, c):
|
||||||
|
if c == '\0':
|
||||||
|
c = '\n'
|
||||||
|
|
||||||
|
self.parse_add_char(c)
|
||||||
|
self.state = IN_QUOTED_FIELD
|
||||||
|
|
||||||
|
def _parse_quote_in_quoted_field(self, c):
|
||||||
|
if (self.dialect.quoting != QUOTE_NONE and
|
||||||
|
c == self.dialect.quotechar):
|
||||||
|
# save "" as "
|
||||||
|
self.parse_add_char(c)
|
||||||
|
self.state = IN_QUOTED_FIELD
|
||||||
|
elif c == self.dialect.delimiter:
|
||||||
|
self.parse_save_field()
|
||||||
|
self.state = START_FIELD
|
||||||
|
elif c == '\n' or c == '\r' or c == '\0':
|
||||||
|
# End of line = return [fields]
|
||||||
|
self.parse_save_field()
|
||||||
|
self.state = START_RECORD if c == '\0' else EAT_CRNL
|
||||||
|
elif not self.dialect.strict:
|
||||||
|
self.parse_add_char(c)
|
||||||
|
self.state = IN_FIELD
|
||||||
|
else:
|
||||||
|
# illegal
|
||||||
|
raise Error("{delimiter}' expected after '{quotechar}".format(
|
||||||
|
delimiter=self.dialect.delimiter,
|
||||||
|
quotechar=self.dialect.quotechar,
|
||||||
|
))
|
||||||
|
|
||||||
|
def _parse_eat_crnl(self, c):
|
||||||
|
if c == '\n' or c == '\r':
|
||||||
|
pass
|
||||||
|
elif c == '\0':
|
||||||
|
self.state = START_RECORD
|
||||||
|
else:
|
||||||
|
raise Error('new-line character seen in unquoted field - do you '
|
||||||
|
'need to open the file in universal-newline mode?')
|
||||||
|
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
self.parse_reset()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
lineobj = next(self.input_iter)
|
||||||
|
except StopIteration:
|
||||||
|
if len(self.field) != 0 or self.state == IN_QUOTED_FIELD:
|
||||||
|
if self.dialect.strict:
|
||||||
|
raise Error('unexpected end of data')
|
||||||
|
self.parse_save_field()
|
||||||
|
if self.fields:
|
||||||
|
break
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not isinstance(lineobj, text_type):
|
||||||
|
typ = type(lineobj)
|
||||||
|
typ_name = 'bytes' if typ == bytes else typ.__name__
|
||||||
|
err_str = ('iterator should return strings, not {0}'
|
||||||
|
' (did you open the file in text mode?)')
|
||||||
|
raise Error(err_str.format(typ_name))
|
||||||
|
|
||||||
|
self.line_num += 1
|
||||||
|
for c in lineobj:
|
||||||
|
if c == '\0':
|
||||||
|
raise Error('line contains NULL byte')
|
||||||
|
self.parse_process_char(c)
|
||||||
|
|
||||||
|
self.parse_process_char('\0')
|
||||||
|
|
||||||
|
if self.state == START_RECORD:
|
||||||
|
break
|
||||||
|
|
||||||
|
fields = self.fields
|
||||||
|
self.fields = None
|
||||||
|
return fields
|
||||||
|
|
||||||
|
next = __next__
|
||||||
|
|
||||||
|
|
||||||
|
_dialect_registry = {}
|
||||||
|
def register_dialect(name, dialect='excel', **fmtparams):
|
||||||
|
if not isinstance(name, text_type):
|
||||||
|
raise TypeError('"name" must be a string')
|
||||||
|
|
||||||
|
dialect = Dialect.extend(dialect, fmtparams)
|
||||||
|
|
||||||
|
try:
|
||||||
|
Dialect.validate(dialect)
|
||||||
|
except:
|
||||||
|
raise TypeError('dialect is invalid')
|
||||||
|
|
||||||
|
assert name not in _dialect_registry
|
||||||
|
_dialect_registry[name] = dialect
|
||||||
|
|
||||||
|
def unregister_dialect(name):
|
||||||
|
try:
|
||||||
|
_dialect_registry.pop(name)
|
||||||
|
except KeyError:
|
||||||
|
raise Error('"{name}" not a registered dialect'.format(name=name))
|
||||||
|
|
||||||
|
def get_dialect(name):
|
||||||
|
try:
|
||||||
|
return _dialect_registry[name]
|
||||||
|
except KeyError:
|
||||||
|
raise Error('Could not find dialect {0}'.format(name))
|
||||||
|
|
||||||
|
def list_dialects():
|
||||||
|
return list(_dialect_registry)
|
||||||
|
|
||||||
|
|
||||||
|
class Dialect(object):
|
||||||
|
"""Describe a CSV dialect.
|
||||||
|
This must be subclassed (see csv.excel). Valid attributes are:
|
||||||
|
delimiter, quotechar, escapechar, doublequote, skipinitialspace,
|
||||||
|
lineterminator, quoting, strict.
|
||||||
|
"""
|
||||||
|
_name = ""
|
||||||
|
_valid = False
|
||||||
|
# placeholders
|
||||||
|
delimiter = None
|
||||||
|
quotechar = None
|
||||||
|
escapechar = None
|
||||||
|
doublequote = None
|
||||||
|
skipinitialspace = None
|
||||||
|
lineterminator = None
|
||||||
|
quoting = None
|
||||||
|
strict = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.validate(self)
|
||||||
|
if self.__class__ != Dialect:
|
||||||
|
self._valid = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, dialect):
|
||||||
|
dialect = cls.extend(dialect)
|
||||||
|
|
||||||
|
if not isinstance(dialect.quoting, int):
|
||||||
|
raise Error('"quoting" must be an integer')
|
||||||
|
|
||||||
|
if dialect.delimiter is None:
|
||||||
|
raise Error('delimiter must be set')
|
||||||
|
cls.validate_text(dialect, 'delimiter')
|
||||||
|
|
||||||
|
if dialect.lineterminator is None:
|
||||||
|
raise Error('lineterminator must be set')
|
||||||
|
if not isinstance(dialect.lineterminator, text_type):
|
||||||
|
raise Error('"lineterminator" must be a string')
|
||||||
|
|
||||||
|
if dialect.quoting not in [
|
||||||
|
QUOTE_NONE, QUOTE_MINIMAL, QUOTE_NONNUMERIC, QUOTE_ALL]:
|
||||||
|
raise Error('Invalid quoting specified')
|
||||||
|
|
||||||
|
if dialect.quoting != QUOTE_NONE:
|
||||||
|
if dialect.quotechar is None and dialect.escapechar is None:
|
||||||
|
raise Error('quotechar must be set if quoting enabled')
|
||||||
|
if dialect.quotechar is not None:
|
||||||
|
cls.validate_text(dialect, 'quotechar')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_text(dialect, attr):
|
||||||
|
val = getattr(dialect, attr)
|
||||||
|
if not isinstance(val, text_type):
|
||||||
|
if type(val) == bytes:
|
||||||
|
raise Error('"{0}" must be string, not bytes'.format(attr))
|
||||||
|
raise Error('"{0}" must be string, not {1}'.format(
|
||||||
|
attr, type(val).__name__))
|
||||||
|
|
||||||
|
if len(val) != 1:
|
||||||
|
raise Error('"{0}" must be a 1-character string'.format(attr))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def defaults():
|
||||||
|
return {
|
||||||
|
'delimiter': ',',
|
||||||
|
'doublequote': True,
|
||||||
|
'escapechar': None,
|
||||||
|
'lineterminator': '\r\n',
|
||||||
|
'quotechar': '"',
|
||||||
|
'quoting': QUOTE_MINIMAL,
|
||||||
|
'skipinitialspace': False,
|
||||||
|
'strict': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def extend(cls, dialect, fmtparams=None):
|
||||||
|
if isinstance(dialect, string_types):
|
||||||
|
dialect = get_dialect(dialect)
|
||||||
|
|
||||||
|
if fmtparams is None:
|
||||||
|
return dialect
|
||||||
|
|
||||||
|
defaults = cls.defaults()
|
||||||
|
|
||||||
|
if any(param not in defaults for param in fmtparams):
|
||||||
|
raise TypeError('Invalid fmtparam')
|
||||||
|
|
||||||
|
specified = dict(
|
||||||
|
(attr, getattr(dialect, attr, None))
|
||||||
|
for attr in cls.defaults()
|
||||||
|
)
|
||||||
|
|
||||||
|
specified.update(fmtparams)
|
||||||
|
return type(str('ExtendedDialect'), (cls,), specified)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def combine(cls, dialect, fmtparams):
|
||||||
|
"""Create a new dialect with defaults and added parameters."""
|
||||||
|
dialect = cls.extend(dialect, fmtparams)
|
||||||
|
defaults = cls.defaults()
|
||||||
|
specified = dict(
|
||||||
|
(attr, getattr(dialect, attr, None))
|
||||||
|
for attr in defaults
|
||||||
|
if getattr(dialect, attr, None) is not None or
|
||||||
|
attr in ['quotechar', 'delimiter', 'lineterminator', 'quoting']
|
||||||
|
)
|
||||||
|
|
||||||
|
defaults.update(specified)
|
||||||
|
dialect = type(str('CombinedDialect'), (cls,), defaults)
|
||||||
|
cls.validate(dialect)
|
||||||
|
return dialect()
|
||||||
|
|
||||||
|
def __delattr__(self, attr):
|
||||||
|
if self._valid:
|
||||||
|
raise AttributeError('dialect is immutable.')
|
||||||
|
super(Dialect, self).__delattr__(attr)
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
if self._valid:
|
||||||
|
raise AttributeError('dialect is immutable.')
|
||||||
|
super(Dialect, self).__setattr__(attr, value)
|
||||||
|
|
||||||
|
|
||||||
|
class excel(Dialect):
|
||||||
|
"""Describe the usual properties of Excel-generated CSV files."""
|
||||||
|
delimiter = ','
|
||||||
|
quotechar = '"'
|
||||||
|
doublequote = True
|
||||||
|
skipinitialspace = False
|
||||||
|
lineterminator = '\r\n'
|
||||||
|
quoting = QUOTE_MINIMAL
|
||||||
|
register_dialect("excel", excel)
|
||||||
|
|
||||||
|
class excel_tab(excel):
|
||||||
|
"""Describe the usual properties of Excel-generated TAB-delimited files."""
|
||||||
|
delimiter = '\t'
|
||||||
|
register_dialect("excel-tab", excel_tab)
|
||||||
|
|
||||||
|
class unix_dialect(Dialect):
|
||||||
|
"""Describe the usual properties of Unix-generated CSV files."""
|
||||||
|
delimiter = ','
|
||||||
|
quotechar = '"'
|
||||||
|
doublequote = True
|
||||||
|
skipinitialspace = False
|
||||||
|
lineterminator = '\n'
|
||||||
|
quoting = QUOTE_ALL
|
||||||
|
register_dialect("unix", unix_dialect)
|
||||||
|
|
||||||
|
|
||||||
|
class DictReader(object):
|
||||||
|
def __init__(self, f, fieldnames=None, restkey=None, restval=None,
|
||||||
|
dialect="excel", *args, **kwds):
|
||||||
|
self._fieldnames = fieldnames # list of keys for the dict
|
||||||
|
self.restkey = restkey # key to catch long rows
|
||||||
|
self.restval = restval # default value for short rows
|
||||||
|
self.reader = reader(f, dialect, *args, **kwds)
|
||||||
|
self.dialect = dialect
|
||||||
|
self.line_num = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fieldnames(self):
|
||||||
|
if self._fieldnames is None:
|
||||||
|
try:
|
||||||
|
self._fieldnames = next(self.reader)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
self.line_num = self.reader.line_num
|
||||||
|
return self._fieldnames
|
||||||
|
|
||||||
|
@fieldnames.setter
|
||||||
|
def fieldnames(self, value):
|
||||||
|
self._fieldnames = value
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
if self.line_num == 0:
|
||||||
|
# Used only for its side effect.
|
||||||
|
self.fieldnames
|
||||||
|
row = next(self.reader)
|
||||||
|
self.line_num = self.reader.line_num
|
||||||
|
|
||||||
|
# unlike the basic reader, we prefer not to return blanks,
|
||||||
|
# because we will typically wind up with a dict full of None
|
||||||
|
# values
|
||||||
|
while row == []:
|
||||||
|
row = next(self.reader)
|
||||||
|
d = dict(zip(self.fieldnames, row))
|
||||||
|
lf = len(self.fieldnames)
|
||||||
|
lr = len(row)
|
||||||
|
if lf < lr:
|
||||||
|
d[self.restkey] = row[lf:]
|
||||||
|
elif lf > lr:
|
||||||
|
for key in self.fieldnames[lr:]:
|
||||||
|
d[key] = self.restval
|
||||||
|
return d
|
||||||
|
|
||||||
|
next = __next__
|
||||||
|
|
||||||
|
|
||||||
|
class DictWriter(object):
|
||||||
|
def __init__(self, f, fieldnames, restval="", extrasaction="raise",
|
||||||
|
dialect="excel", *args, **kwds):
|
||||||
|
self.fieldnames = fieldnames # list of keys for the dict
|
||||||
|
self.restval = restval # for writing short dicts
|
||||||
|
if extrasaction.lower() not in ("raise", "ignore"):
|
||||||
|
raise ValueError("extrasaction (%s) must be 'raise' or 'ignore'"
|
||||||
|
% extrasaction)
|
||||||
|
self.extrasaction = extrasaction
|
||||||
|
self.writer = writer(f, dialect, *args, **kwds)
|
||||||
|
|
||||||
|
def writeheader(self):
|
||||||
|
header = dict(zip(self.fieldnames, self.fieldnames))
|
||||||
|
self.writerow(header)
|
||||||
|
|
||||||
|
def _dict_to_list(self, rowdict):
|
||||||
|
if self.extrasaction == "raise":
|
||||||
|
wrong_fields = [k for k in rowdict if k not in self.fieldnames]
|
||||||
|
if wrong_fields:
|
||||||
|
raise ValueError("dict contains fields not in fieldnames: "
|
||||||
|
+ ", ".join([repr(x) for x in wrong_fields]))
|
||||||
|
return (rowdict.get(key, self.restval) for key in self.fieldnames)
|
||||||
|
|
||||||
|
def writerow(self, rowdict):
|
||||||
|
return self.writer.writerow(self._dict_to_list(rowdict))
|
||||||
|
|
||||||
|
def writerows(self, rowdicts):
|
||||||
|
return self.writer.writerows(map(self._dict_to_list, rowdicts))
|
||||||
|
|
||||||
|
# Guard Sniffer's type checking against builds that exclude complex()
|
||||||
|
try:
|
||||||
|
complex
|
||||||
|
except NameError:
|
||||||
|
complex = float
|
||||||
|
|
||||||
|
class Sniffer(object):
|
||||||
|
'''
|
||||||
|
"Sniffs" the format of a CSV file (i.e. delimiter, quotechar)
|
||||||
|
Returns a Dialect object.
|
||||||
|
'''
|
||||||
|
def __init__(self):
|
||||||
|
# in case there is more than one possible delimiter
|
||||||
|
self.preferred = [',', '\t', ';', ' ', ':']
|
||||||
|
|
||||||
|
|
||||||
|
def sniff(self, sample, delimiters=None):
|
||||||
|
"""
|
||||||
|
Returns a dialect (or None) corresponding to the sample
|
||||||
|
"""
|
||||||
|
|
||||||
|
quotechar, doublequote, delimiter, skipinitialspace = \
|
||||||
|
self._guess_quote_and_delimiter(sample, delimiters)
|
||||||
|
if not delimiter:
|
||||||
|
delimiter, skipinitialspace = self._guess_delimiter(sample,
|
||||||
|
delimiters)
|
||||||
|
|
||||||
|
if not delimiter:
|
||||||
|
raise Error("Could not determine delimiter")
|
||||||
|
|
||||||
|
class dialect(Dialect):
|
||||||
|
_name = "sniffed"
|
||||||
|
lineterminator = '\r\n'
|
||||||
|
quoting = QUOTE_MINIMAL
|
||||||
|
# escapechar = ''
|
||||||
|
|
||||||
|
dialect.doublequote = doublequote
|
||||||
|
dialect.delimiter = delimiter
|
||||||
|
# _csv.reader won't accept a quotechar of ''
|
||||||
|
dialect.quotechar = quotechar or '"'
|
||||||
|
dialect.skipinitialspace = skipinitialspace
|
||||||
|
|
||||||
|
return dialect
|
||||||
|
|
||||||
|
|
||||||
|
def _guess_quote_and_delimiter(self, data, delimiters):
|
||||||
|
"""
|
||||||
|
Looks for text enclosed between two identical quotes
|
||||||
|
(the probable quotechar) which are preceded and followed
|
||||||
|
by the same character (the probable delimiter).
|
||||||
|
For example:
|
||||||
|
,'some text',
|
||||||
|
The quote with the most wins, same with the delimiter.
|
||||||
|
If there is no quotechar the delimiter can't be determined
|
||||||
|
this way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
for restr in ('(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\']).*?(?P=quote)(?P=delim)', # ,".*?",
|
||||||
|
'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)', # ".*?",
|
||||||
|
'(?P<delim>>[^\w\n"\'])(?P<space> ?)(?P<quote>["\']).*?(?P=quote)(?:$|\n)', # ,".*?"
|
||||||
|
'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?:$|\n)'): # ".*?" (no delim, no space)
|
||||||
|
regexp = re.compile(restr, re.DOTALL | re.MULTILINE)
|
||||||
|
matches = regexp.findall(data)
|
||||||
|
if matches:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
# (quotechar, doublequote, delimiter, skipinitialspace)
|
||||||
|
return ('', False, None, 0)
|
||||||
|
quotes = {}
|
||||||
|
delims = {}
|
||||||
|
spaces = 0
|
||||||
|
groupindex = regexp.groupindex
|
||||||
|
for m in matches:
|
||||||
|
n = groupindex['quote'] - 1
|
||||||
|
key = m[n]
|
||||||
|
if key:
|
||||||
|
quotes[key] = quotes.get(key, 0) + 1
|
||||||
|
try:
|
||||||
|
n = groupindex['delim'] - 1
|
||||||
|
key = m[n]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
if key and (delimiters is None or key in delimiters):
|
||||||
|
delims[key] = delims.get(key, 0) + 1
|
||||||
|
try:
|
||||||
|
n = groupindex['space'] - 1
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
if m[n]:
|
||||||
|
spaces += 1
|
||||||
|
|
||||||
|
quotechar = max(quotes, key=quotes.get)
|
||||||
|
|
||||||
|
if delims:
|
||||||
|
delim = max(delims, key=delims.get)
|
||||||
|
skipinitialspace = delims[delim] == spaces
|
||||||
|
if delim == '\n': # most likely a file with a single column
|
||||||
|
delim = ''
|
||||||
|
else:
|
||||||
|
# there is *no* delimiter, it's a single column of quoted data
|
||||||
|
delim = ''
|
||||||
|
skipinitialspace = 0
|
||||||
|
|
||||||
|
# if we see an extra quote between delimiters, we've got a
|
||||||
|
# double quoted format
|
||||||
|
dq_regexp = re.compile(
|
||||||
|
r"((%(delim)s)|^)\W*%(quote)s[^%(delim)s\n]*%(quote)s[^%(delim)s\n]*%(quote)s\W*((%(delim)s)|$)" % \
|
||||||
|
{'delim':re.escape(delim), 'quote':quotechar}, re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if dq_regexp.search(data):
|
||||||
|
doublequote = True
|
||||||
|
else:
|
||||||
|
doublequote = False
|
||||||
|
|
||||||
|
return (quotechar, doublequote, delim, skipinitialspace)
|
||||||
|
|
||||||
|
|
||||||
|
def _guess_delimiter(self, data, delimiters):
|
||||||
|
"""
|
||||||
|
The delimiter /should/ occur the same number of times on
|
||||||
|
each row. However, due to malformed data, it may not. We don't want
|
||||||
|
an all or nothing approach, so we allow for small variations in this
|
||||||
|
number.
|
||||||
|
1) build a table of the frequency of each character on every line.
|
||||||
|
2) build a table of frequencies of this frequency (meta-frequency?),
|
||||||
|
e.g. 'x occurred 5 times in 10 rows, 6 times in 1000 rows,
|
||||||
|
7 times in 2 rows'
|
||||||
|
3) use the mode of the meta-frequency to determine the /expected/
|
||||||
|
frequency for that character
|
||||||
|
4) find out how often the character actually meets that goal
|
||||||
|
5) the character that best meets its goal is the delimiter
|
||||||
|
For performance reasons, the data is evaluated in chunks, so it can
|
||||||
|
try and evaluate the smallest portion of the data possible, evaluating
|
||||||
|
additional chunks as necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = list(filter(None, data.split('\n')))
|
||||||
|
|
||||||
|
ascii = [unichr(c) for c in range(127)] # 7-bit ASCII
|
||||||
|
|
||||||
|
# build frequency tables
|
||||||
|
chunkLength = min(10, len(data))
|
||||||
|
iteration = 0
|
||||||
|
charFrequency = {}
|
||||||
|
modes = {}
|
||||||
|
delims = {}
|
||||||
|
start, end = 0, min(chunkLength, len(data))
|
||||||
|
while start < len(data):
|
||||||
|
iteration += 1
|
||||||
|
for line in data[start:end]:
|
||||||
|
for char in ascii:
|
||||||
|
metaFrequency = charFrequency.get(char, {})
|
||||||
|
# must count even if frequency is 0
|
||||||
|
freq = line.count(char)
|
||||||
|
# value is the mode
|
||||||
|
metaFrequency[freq] = metaFrequency.get(freq, 0) + 1
|
||||||
|
charFrequency[char] = metaFrequency
|
||||||
|
|
||||||
|
for char in charFrequency.keys():
|
||||||
|
items = list(charFrequency[char].items())
|
||||||
|
if len(items) == 1 and items[0][0] == 0:
|
||||||
|
continue
|
||||||
|
# get the mode of the frequencies
|
||||||
|
if len(items) > 1:
|
||||||
|
modes[char] = max(items, key=lambda x: x[1])
|
||||||
|
# adjust the mode - subtract the sum of all
|
||||||
|
# other frequencies
|
||||||
|
items.remove(modes[char])
|
||||||
|
modes[char] = (modes[char][0], modes[char][1]
|
||||||
|
- sum(item[1] for item in items))
|
||||||
|
else:
|
||||||
|
modes[char] = items[0]
|
||||||
|
|
||||||
|
# build a list of possible delimiters
|
||||||
|
modeList = modes.items()
|
||||||
|
total = float(chunkLength * iteration)
|
||||||
|
# (rows of consistent data) / (number of rows) = 100%
|
||||||
|
consistency = 1.0
|
||||||
|
# minimum consistency threshold
|
||||||
|
threshold = 0.9
|
||||||
|
while len(delims) == 0 and consistency >= threshold:
|
||||||
|
for k, v in modeList:
|
||||||
|
if v[0] > 0 and v[1] > 0:
|
||||||
|
if ((v[1]/total) >= consistency and
|
||||||
|
(delimiters is None or k in delimiters)):
|
||||||
|
delims[k] = v
|
||||||
|
consistency -= 0.01
|
||||||
|
|
||||||
|
if len(delims) == 1:
|
||||||
|
delim = list(delims.keys())[0]
|
||||||
|
skipinitialspace = (data[0].count(delim) ==
|
||||||
|
data[0].count("%c " % delim))
|
||||||
|
return (delim, skipinitialspace)
|
||||||
|
|
||||||
|
# analyze another chunkLength lines
|
||||||
|
start = end
|
||||||
|
end += chunkLength
|
||||||
|
|
||||||
|
if not delims:
|
||||||
|
return ('', 0)
|
||||||
|
|
||||||
|
# if there's more than one, fall back to a 'preferred' list
|
||||||
|
if len(delims) > 1:
|
||||||
|
for d in self.preferred:
|
||||||
|
if d in delims.keys():
|
||||||
|
skipinitialspace = (data[0].count(d) ==
|
||||||
|
data[0].count("%c " % d))
|
||||||
|
return (d, skipinitialspace)
|
||||||
|
|
||||||
|
# nothing else indicates a preference, pick the character that
|
||||||
|
# dominates(?)
|
||||||
|
items = [(v,k) for (k,v) in delims.items()]
|
||||||
|
items.sort()
|
||||||
|
delim = items[-1][1]
|
||||||
|
|
||||||
|
skipinitialspace = (data[0].count(delim) ==
|
||||||
|
data[0].count("%c " % delim))
|
||||||
|
return (delim, skipinitialspace)
|
||||||
|
|
||||||
|
|
||||||
|
def has_header(self, sample):
|
||||||
|
# Creates a dictionary of types of data in each column. If any
|
||||||
|
# column is of a single type (say, integers), *except* for the first
|
||||||
|
# row, then the first row is presumed to be labels. If the type
|
||||||
|
# can't be determined, it is assumed to be a string in which case
|
||||||
|
# the length of the string is the determining factor: if all of the
|
||||||
|
# rows except for the first are the same length, it's a header.
|
||||||
|
# Finally, a 'vote' is taken at the end for each column, adding or
|
||||||
|
# subtracting from the likelihood of the first row being a header.
|
||||||
|
|
||||||
|
rdr = reader(StringIO(sample), self.sniff(sample))
|
||||||
|
|
||||||
|
header = next(rdr) # assume first row is header
|
||||||
|
|
||||||
|
columns = len(header)
|
||||||
|
columnTypes = {}
|
||||||
|
for i in range(columns): columnTypes[i] = None
|
||||||
|
|
||||||
|
checked = 0
|
||||||
|
for row in rdr:
|
||||||
|
# arbitrary number of rows to check, to keep it sane
|
||||||
|
if checked > 20:
|
||||||
|
break
|
||||||
|
checked += 1
|
||||||
|
|
||||||
|
if len(row) != columns:
|
||||||
|
continue # skip rows that have irregular number of columns
|
||||||
|
|
||||||
|
for col in list(columnTypes.keys()):
|
||||||
|
|
||||||
|
for thisType in [int, float, complex]:
|
||||||
|
try:
|
||||||
|
thisType(row[col])
|
||||||
|
break
|
||||||
|
except (ValueError, OverflowError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# fallback to length of string
|
||||||
|
thisType = len(row[col])
|
||||||
|
|
||||||
|
if thisType != columnTypes[col]:
|
||||||
|
if columnTypes[col] is None: # add new column type
|
||||||
|
columnTypes[col] = thisType
|
||||||
|
else:
|
||||||
|
# type is inconsistent, remove column from
|
||||||
|
# consideration
|
||||||
|
del columnTypes[col]
|
||||||
|
|
||||||
|
# finally, compare results against first row and "vote"
|
||||||
|
# on whether it's a header
|
||||||
|
hasHeader = 0
|
||||||
|
for col, colType in columnTypes.items():
|
||||||
|
if type(colType) == type(0): # it's a length
|
||||||
|
if len(header[col]) != colType:
|
||||||
|
hasHeader += 1
|
||||||
|
else:
|
||||||
|
hasHeader -= 1
|
||||||
|
else: # attempt typecast
|
||||||
|
try:
|
||||||
|
colType(header[col])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
hasHeader += 1
|
||||||
|
else:
|
||||||
|
hasHeader -= 1
|
||||||
|
|
||||||
|
return hasHeader > 0
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
from .core import where
|
from .core import contents, where
|
||||||
|
|
||||||
__version__ = "2019.11.28"
|
__version__ = "2020.11.08"
|
||||||
|
|||||||
@@ -1,2 +1,12 @@
|
|||||||
from certifi import where
|
import argparse
|
||||||
|
|
||||||
|
from certifi import contents, where
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-c", "--contents", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.contents:
|
||||||
|
print(contents())
|
||||||
|
else:
|
||||||
print(where())
|
print(where())
|
||||||
|
|||||||
@@ -58,38 +58,6 @@ AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
|||||||
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only
|
|
||||||
# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only
|
|
||||||
# Label: "Verisign Class 3 Public Primary Certification Authority - G3"
|
|
||||||
# Serial: 206684696279472310254277870180966723415
|
|
||||||
# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09
|
|
||||||
# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6
|
|
||||||
# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
|
|
||||||
CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
|
|
||||||
cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
|
|
||||||
LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
|
|
||||||
aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
|
||||||
dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
|
|
||||||
VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
|
|
||||||
aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
|
|
||||||
bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
|
|
||||||
IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
|
|
||||||
LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
|
|
||||||
N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
|
|
||||||
KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
|
|
||||||
kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
|
|
||||||
CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
|
|
||||||
Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
|
|
||||||
imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
|
|
||||||
2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
|
|
||||||
DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
|
|
||||||
/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
|
|
||||||
F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
|
|
||||||
TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
|
# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
|
||||||
# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
|
# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
|
||||||
# Label: "Entrust.net Premium 2048 Secure Server CA"
|
# Label: "Entrust.net Premium 2048 Secure Server CA"
|
||||||
@@ -152,39 +120,6 @@ ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
|
|||||||
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
|
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network
|
|
||||||
# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network
|
|
||||||
# Label: "AddTrust External Root"
|
|
||||||
# Serial: 1
|
|
||||||
# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f
|
|
||||||
# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68
|
|
||||||
# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
|
|
||||||
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
|
|
||||||
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
|
|
||||||
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
|
|
||||||
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
|
|
||||||
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
|
|
||||||
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
|
|
||||||
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
|
|
||||||
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
|
|
||||||
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
|
|
||||||
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
|
|
||||||
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
|
|
||||||
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
|
|
||||||
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
|
|
||||||
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
|
|
||||||
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
|
|
||||||
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
|
|
||||||
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
|
|
||||||
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
|
|
||||||
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
|
|
||||||
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
|
||||||
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
|
||||||
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
|
# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
|
||||||
# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
|
# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
|
||||||
# Label: "Entrust Root Certification Authority"
|
# Label: "Entrust Root Certification Authority"
|
||||||
@@ -640,46 +575,6 @@ VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
|
|||||||
WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
|
WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: O=Government Root Certification Authority
|
|
||||||
# Subject: O=Government Root Certification Authority
|
|
||||||
# Label: "Taiwan GRCA"
|
|
||||||
# Serial: 42023070807708724159991140556527066870
|
|
||||||
# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e
|
|
||||||
# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9
|
|
||||||
# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/
|
|
||||||
MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj
|
|
||||||
YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow
|
|
||||||
PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp
|
|
||||||
Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
|
||||||
AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR
|
|
||||||
IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q
|
|
||||||
gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy
|
|
||||||
yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts
|
|
||||||
F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2
|
|
||||||
jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx
|
|
||||||
ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC
|
|
||||||
VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK
|
|
||||||
YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH
|
|
||||||
EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN
|
|
||||||
Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud
|
|
||||||
DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE
|
|
||||||
MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK
|
|
||||||
UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ
|
|
||||||
TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf
|
|
||||||
qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK
|
|
||||||
ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE
|
|
||||||
JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7
|
|
||||||
hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1
|
|
||||||
EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm
|
|
||||||
nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX
|
|
||||||
udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz
|
|
||||||
ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe
|
|
||||||
LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl
|
|
||||||
pYYsfPQS
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
||||||
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
||||||
# Label: "DigiCert Assured ID Root CA"
|
# Label: "DigiCert Assured ID Root CA"
|
||||||
@@ -1127,38 +1022,6 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
|
|||||||
GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
|
GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed
|
|
||||||
# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed
|
|
||||||
# Label: "OISTE WISeKey Global Root GA CA"
|
|
||||||
# Serial: 86718877871133159090080555911823548314
|
|
||||||
# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93
|
|
||||||
# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9
|
|
||||||
# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB
|
|
||||||
ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly
|
|
||||||
aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl
|
|
||||||
ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w
|
|
||||||
NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G
|
|
||||||
A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD
|
|
||||||
VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX
|
|
||||||
SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
|
||||||
MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR
|
|
||||||
VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2
|
|
||||||
w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF
|
|
||||||
mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg
|
|
||||||
4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9
|
|
||||||
4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw
|
|
||||||
DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw
|
|
||||||
EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx
|
|
||||||
SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2
|
|
||||||
ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8
|
|
||||||
vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa
|
|
||||||
hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi
|
|
||||||
Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ
|
|
||||||
/L7fCg0=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=Certigna O=Dhimyotis
|
# Issuer: CN=Certigna O=Dhimyotis
|
||||||
# Subject: CN=Certigna O=Dhimyotis
|
# Subject: CN=Certigna O=Dhimyotis
|
||||||
# Label: "Certigna"
|
# Label: "Certigna"
|
||||||
@@ -1499,47 +1362,6 @@ uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
|
|||||||
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
|
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden
|
|
||||||
# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden
|
|
||||||
# Label: "Staat der Nederlanden Root CA - G2"
|
|
||||||
# Serial: 10000012
|
|
||||||
# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a
|
|
||||||
# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16
|
|
||||||
# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO
|
|
||||||
TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh
|
|
||||||
dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX
|
|
||||||
DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl
|
|
||||||
ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv
|
|
||||||
b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291
|
|
||||||
qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp
|
|
||||||
uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU
|
|
||||||
Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE
|
|
||||||
pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp
|
|
||||||
5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M
|
|
||||||
UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN
|
|
||||||
GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy
|
|
||||||
5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv
|
|
||||||
6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK
|
|
||||||
eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6
|
|
||||||
B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/
|
|
||||||
BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov
|
|
||||||
L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV
|
|
||||||
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG
|
|
||||||
SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS
|
|
||||||
CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen
|
|
||||||
5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897
|
|
||||||
IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK
|
|
||||||
gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL
|
|
||||||
+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL
|
|
||||||
vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm
|
|
||||||
bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk
|
|
||||||
N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC
|
|
||||||
Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z
|
|
||||||
ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post
|
# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post
|
||||||
# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post
|
# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post
|
||||||
# Label: "Hongkong Post Root CA 1"
|
# Label: "Hongkong Post Root CA 1"
|
||||||
@@ -2140,6 +1962,45 @@ t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy
|
|||||||
SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
|
SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes
|
||||||
|
# Subject: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes
|
||||||
|
# Label: "EC-ACC"
|
||||||
|
# Serial: -23701579247955709139626555126524820479
|
||||||
|
# MD5 Fingerprint: eb:f5:9d:29:0d:61:f9:42:1f:7c:c2:ba:6d:e3:15:09
|
||||||
|
# SHA1 Fingerprint: 28:90:3a:63:5b:52:80:fa:e6:77:4c:0b:6d:a7:d6:ba:a6:4a:f2:e8
|
||||||
|
# SHA256 Fingerprint: 88:49:7f:01:60:2f:31:54:24:6a:e2:8c:4d:5a:ef:10:f1:d8:7e:bb:76:62:6f:4a:e0:b7:f9:5b:a7:96:87:99
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB
|
||||||
|
8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy
|
||||||
|
dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1
|
||||||
|
YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3
|
||||||
|
dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh
|
||||||
|
IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD
|
||||||
|
LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG
|
||||||
|
EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g
|
||||||
|
KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD
|
||||||
|
ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu
|
||||||
|
bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg
|
||||||
|
ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN
|
||||||
|
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R
|
||||||
|
85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm
|
||||||
|
4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV
|
||||||
|
HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd
|
||||||
|
QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t
|
||||||
|
lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB
|
||||||
|
o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E
|
||||||
|
BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4
|
||||||
|
opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo
|
||||||
|
dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW
|
||||||
|
ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN
|
||||||
|
AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y
|
||||||
|
/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k
|
||||||
|
SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy
|
||||||
|
Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS
|
||||||
|
Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl
|
||||||
|
nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
|
# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
|
||||||
# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
|
# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
|
||||||
# Label: "Hellenic Academic and Research Institutions RootCA 2011"
|
# Label: "Hellenic Academic and Research Institutions RootCA 2011"
|
||||||
@@ -2352,38 +2213,6 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
|
|||||||
TpPDpFQUWw==
|
TpPDpFQUWw==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus
|
|
||||||
# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus
|
|
||||||
# Label: "EE Certification Centre Root CA"
|
|
||||||
# Serial: 112324828676200291871926431888494945866
|
|
||||||
# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f
|
|
||||||
# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7
|
|
||||||
# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1
|
|
||||||
MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
|
|
||||||
czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG
|
|
||||||
CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy
|
|
||||||
MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl
|
|
||||||
ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS
|
|
||||||
b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB
|
|
||||||
AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy
|
|
||||||
euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO
|
|
||||||
bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw
|
|
||||||
WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d
|
|
||||||
MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE
|
|
||||||
1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD
|
|
||||||
VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/
|
|
||||||
zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB
|
|
||||||
BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF
|
|
||||||
BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV
|
|
||||||
v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG
|
|
||||||
E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u
|
|
||||||
uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW
|
|
||||||
iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v
|
|
||||||
GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
|
# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
|
||||||
# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
|
# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
|
||||||
# Label: "D-TRUST Root Class 3 CA 2 2009"
|
# Label: "D-TRUST Root Class 3 CA 2 2009"
|
||||||
@@ -3749,47 +3578,6 @@ CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW
|
|||||||
1KyLa2tJElMzrdfkviT8tQp21KW8EA==
|
1KyLa2tJElMzrdfkviT8tQp21KW8EA==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=LuxTrust Global Root 2 O=LuxTrust S.A.
|
|
||||||
# Subject: CN=LuxTrust Global Root 2 O=LuxTrust S.A.
|
|
||||||
# Label: "LuxTrust Global Root 2"
|
|
||||||
# Serial: 59914338225734147123941058376788110305822489521
|
|
||||||
# MD5 Fingerprint: b2:e1:09:00:61:af:f7:f1:91:6f:c4:ad:8d:5e:3b:7c
|
|
||||||
# SHA1 Fingerprint: 1e:0e:56:19:0a:d1:8b:25:98:b2:04:44:ff:66:8a:04:17:99:5f:3f
|
|
||||||
# SHA256 Fingerprint: 54:45:5f:71:29:c2:0b:14:47:c4:18:f9:97:16:8f:24:c5:8f:c5:02:3b:f5:da:5b:e2:eb:6e:1d:d8:90:2e:d5
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV
|
|
||||||
BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw
|
|
||||||
MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B
|
|
||||||
LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN
|
|
||||||
AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F
|
|
||||||
ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem
|
|
||||||
hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1
|
|
||||||
EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn
|
|
||||||
Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4
|
|
||||||
zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ
|
|
||||||
96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m
|
|
||||||
j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g
|
|
||||||
DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+
|
|
||||||
8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j
|
|
||||||
X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH
|
|
||||||
hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB
|
|
||||||
KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0
|
|
||||||
Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT
|
|
||||||
+Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL
|
|
||||||
BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9
|
|
||||||
BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO
|
|
||||||
jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9
|
|
||||||
loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c
|
|
||||||
qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+
|
|
||||||
2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/
|
|
||||||
JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre
|
|
||||||
zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf
|
|
||||||
LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+
|
|
||||||
x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6
|
|
||||||
oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM
|
# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM
|
||||||
# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM
|
# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM
|
||||||
# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1"
|
# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1"
|
||||||
@@ -4600,3 +4388,219 @@ IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
|
|||||||
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
||||||
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation
|
||||||
|
# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation
|
||||||
|
# Label: "Microsoft ECC Root Certificate Authority 2017"
|
||||||
|
# Serial: 136839042543790627607696632466672567020
|
||||||
|
# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67
|
||||||
|
# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5
|
||||||
|
# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw
|
||||||
|
CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD
|
||||||
|
VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw
|
||||||
|
MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV
|
||||||
|
UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy
|
||||||
|
b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq
|
||||||
|
hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR
|
||||||
|
ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb
|
||||||
|
hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E
|
||||||
|
BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3
|
||||||
|
FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV
|
||||||
|
L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB
|
||||||
|
iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation
|
||||||
|
# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation
|
||||||
|
# Label: "Microsoft RSA Root Certificate Authority 2017"
|
||||||
|
# Serial: 40975477897264996090493496164228220339
|
||||||
|
# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47
|
||||||
|
# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74
|
||||||
|
# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl
|
||||||
|
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw
|
||||||
|
NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
|
||||||
|
IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG
|
||||||
|
EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N
|
||||||
|
aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi
|
||||||
|
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ
|
||||||
|
Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0
|
||||||
|
ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1
|
||||||
|
HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm
|
||||||
|
gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ
|
||||||
|
jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc
|
||||||
|
aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG
|
||||||
|
YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6
|
||||||
|
W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K
|
||||||
|
UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH
|
||||||
|
+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q
|
||||||
|
W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/
|
||||||
|
BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC
|
||||||
|
NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC
|
||||||
|
LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC
|
||||||
|
gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6
|
||||||
|
tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh
|
||||||
|
SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2
|
||||||
|
TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3
|
||||||
|
pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR
|
||||||
|
xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp
|
||||||
|
GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9
|
||||||
|
dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN
|
||||||
|
AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB
|
||||||
|
RA+GsCyRxj3qrg+E
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd.
|
||||||
|
# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd.
|
||||||
|
# Label: "e-Szigno Root CA 2017"
|
||||||
|
# Serial: 411379200276854331539784714
|
||||||
|
# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98
|
||||||
|
# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1
|
||||||
|
# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV
|
||||||
|
BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk
|
||||||
|
LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv
|
||||||
|
b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ
|
||||||
|
BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg
|
||||||
|
THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v
|
||||||
|
IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv
|
||||||
|
xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H
|
||||||
|
Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
|
||||||
|
A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB
|
||||||
|
eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo
|
||||||
|
jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ
|
||||||
|
+efcMQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2
|
||||||
|
# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2
|
||||||
|
# Label: "certSIGN Root CA G2"
|
||||||
|
# Serial: 313609486401300475190
|
||||||
|
# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7
|
||||||
|
# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32
|
||||||
|
# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
|
||||||
|
BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g
|
||||||
|
Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ
|
||||||
|
BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ
|
||||||
|
R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF
|
||||||
|
dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw
|
||||||
|
vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ
|
||||||
|
uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp
|
||||||
|
n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs
|
||||||
|
cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW
|
||||||
|
xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P
|
||||||
|
rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF
|
||||||
|
DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx
|
||||||
|
DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy
|
||||||
|
LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C
|
||||||
|
eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB
|
||||||
|
/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ
|
||||||
|
d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq
|
||||||
|
kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC
|
||||||
|
b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl
|
||||||
|
qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0
|
||||||
|
OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c
|
||||||
|
NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk
|
||||||
|
ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO
|
||||||
|
pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj
|
||||||
|
03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk
|
||||||
|
PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE
|
||||||
|
1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX
|
||||||
|
QRBdJ3NghVdJIgc=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc.
|
||||||
|
# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc.
|
||||||
|
# Label: "Trustwave Global Certification Authority"
|
||||||
|
# Serial: 1846098327275375458322922162
|
||||||
|
# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e
|
||||||
|
# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5
|
||||||
|
# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw
|
||||||
|
CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x
|
||||||
|
ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1
|
||||||
|
c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx
|
||||||
|
OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI
|
||||||
|
SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI
|
||||||
|
b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp
|
||||||
|
Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||||
|
ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn
|
||||||
|
swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu
|
||||||
|
7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8
|
||||||
|
1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW
|
||||||
|
80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP
|
||||||
|
JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l
|
||||||
|
RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw
|
||||||
|
hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10
|
||||||
|
coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc
|
||||||
|
BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n
|
||||||
|
twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud
|
||||||
|
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud
|
||||||
|
DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W
|
||||||
|
0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe
|
||||||
|
uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q
|
||||||
|
lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB
|
||||||
|
aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE
|
||||||
|
sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT
|
||||||
|
MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe
|
||||||
|
qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh
|
||||||
|
VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8
|
||||||
|
h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9
|
||||||
|
EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK
|
||||||
|
yeC2nOnOcXHebD8WpHk=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc.
|
||||||
|
# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc.
|
||||||
|
# Label: "Trustwave Global ECC P256 Certification Authority"
|
||||||
|
# Serial: 4151900041497450638097112925
|
||||||
|
# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54
|
||||||
|
# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf
|
||||||
|
# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD
|
||||||
|
VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf
|
||||||
|
BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3
|
||||||
|
YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x
|
||||||
|
NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G
|
||||||
|
A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0
|
||||||
|
d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF
|
||||||
|
Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG
|
||||||
|
SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN
|
||||||
|
FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w
|
||||||
|
DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw
|
||||||
|
CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh
|
||||||
|
DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc.
|
||||||
|
# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc.
|
||||||
|
# Label: "Trustwave Global ECC P384 Certification Authority"
|
||||||
|
# Serial: 2704997926503831671788816187
|
||||||
|
# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6
|
||||||
|
# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2
|
||||||
|
# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD
|
||||||
|
VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf
|
||||||
|
BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3
|
||||||
|
YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x
|
||||||
|
NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G
|
||||||
|
A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0
|
||||||
|
d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF
|
||||||
|
Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB
|
||||||
|
BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ
|
||||||
|
j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF
|
||||||
|
1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G
|
||||||
|
A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3
|
||||||
|
AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC
|
||||||
|
MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu
|
||||||
|
Sw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -4,12 +4,57 @@
|
|||||||
certifi.py
|
certifi.py
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
This module returns the installation location of cacert.pem.
|
This module returns the installation location of cacert.pem or its contents.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
from importlib.resources import path as get_path, read_text
|
||||||
|
|
||||||
|
_CACERT_CTX = None
|
||||||
|
_CACERT_PATH = None
|
||||||
|
|
||||||
|
def where():
|
||||||
|
# This is slightly terrible, but we want to delay extracting the file
|
||||||
|
# in cases where we're inside of a zipimport situation until someone
|
||||||
|
# actually calls where(), but we don't want to re-extract the file
|
||||||
|
# on every call of where(), so we'll do it once then store it in a
|
||||||
|
# global variable.
|
||||||
|
global _CACERT_CTX
|
||||||
|
global _CACERT_PATH
|
||||||
|
if _CACERT_PATH is None:
|
||||||
|
# This is slightly janky, the importlib.resources API wants you to
|
||||||
|
# manage the cleanup of this file, so it doesn't actually return a
|
||||||
|
# path, it returns a context manager that will give you the path
|
||||||
|
# when you enter it and will do any cleanup when you leave it. In
|
||||||
|
# the common case of not needing a temporary file, it will just
|
||||||
|
# return the file system location and the __exit__() is a no-op.
|
||||||
|
#
|
||||||
|
# We also have to hold onto the actual context manager, because
|
||||||
|
# it will do the cleanup whenever it gets garbage collected, so
|
||||||
|
# we will also store that at the global level as well.
|
||||||
|
_CACERT_CTX = get_path("certifi", "cacert.pem")
|
||||||
|
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||||
|
|
||||||
|
return _CACERT_PATH
|
||||||
|
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# This fallback will work for Python versions prior to 3.7 that lack the
|
||||||
|
# importlib.resources module but relies on the existing `where` function
|
||||||
|
# so won't address issues with environments like PyOxidizer that don't set
|
||||||
|
# __file__ on modules.
|
||||||
|
def read_text(_module, _path, encoding="ascii"):
|
||||||
|
with open(where(), "r", encoding=encoding) as data:
|
||||||
|
return data.read()
|
||||||
|
|
||||||
|
# If we don't have importlib.resources, then we will just do the old logic
|
||||||
|
# of assuming we're on the filesystem and munge the path directly.
|
||||||
def where():
|
def where():
|
||||||
f = os.path.dirname(__file__)
|
f = os.path.dirname(__file__)
|
||||||
|
|
||||||
return os.path.join(f, 'cacert.pem')
|
return os.path.join(f, "cacert.pem")
|
||||||
|
|
||||||
|
|
||||||
|
def contents():
|
||||||
|
return read_text("certifi", "cacert.pem", encoding="ascii")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class AlertListener(threading.Thread):
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stop the AlertListener thread. Once the notifier is stopped, it cannot be directly
|
""" Stop the AlertListener thread. Once the notifier is stopped, it cannot be directly
|
||||||
started again. You must call :func:`plexapi.server.PlexServer.startAlertListener()`
|
started again. You must call :func:`~plexapi.server.PlexServer.startAlertListener`
|
||||||
from a PlexServer instance.
|
from a PlexServer instance.
|
||||||
"""
|
"""
|
||||||
log.info('Stopping AlertListener.')
|
log.info('Stopping AlertListener.')
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class Audio(PlexPartialObject):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
addedAt (datetime): Datetime this item was added to the library.
|
addedAt (datetime): Datetime this item was added to the library.
|
||||||
|
art (str): URL to artwork image.
|
||||||
|
artBlurHash (str): BlurHash string for artwork image.
|
||||||
index (sting): Index Number (often the track number).
|
index (sting): Index Number (often the track number).
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
lastViewedAt (datetime): Datetime item was last accessed.
|
lastViewedAt (datetime): Datetime item was last accessed.
|
||||||
@@ -18,6 +20,7 @@ class Audio(PlexPartialObject):
|
|||||||
ratingKey (int): Unique key identifying this item.
|
ratingKey (int): Unique key identifying this item.
|
||||||
summary (str): Summary of the artist, track, or album.
|
summary (str): Summary of the artist, track, or album.
|
||||||
thumb (str): URL to thumbnail image.
|
thumb (str): URL to thumbnail image.
|
||||||
|
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||||
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'artist', 'album', or 'track'.
|
type (str): 'artist', 'album', or 'track'.
|
||||||
@@ -32,13 +35,18 @@ class Audio(PlexPartialObject):
|
|||||||
self._data = data
|
self._data = data
|
||||||
self.listType = 'audio'
|
self.listType = 'audio'
|
||||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||||
|
self.art = data.attrib.get('art')
|
||||||
|
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||||
self.index = data.attrib.get('index')
|
self.index = data.attrib.get('index')
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||||
|
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
@@ -67,20 +75,20 @@ class Audio(PlexPartialObject):
|
|||||||
|
|
||||||
def sync(self, bitrate, client=None, clientId=None, limit=None, title=None):
|
def sync(self, bitrate, client=None, clientId=None, limit=None, title=None):
|
||||||
""" Add current audio (artist, album or track) as sync item for specified device.
|
""" Add current audio (artist, album or track) as sync item for specified device.
|
||||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
|
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
|
||||||
module :mod:`plexapi.sync`.
|
module :mod:`~plexapi.sync`.
|
||||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
:func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
limit (int): maximum count of items to sync, unlimited if `None`.
|
limit (int): maximum count of items to sync, unlimited if `None`.
|
||||||
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
|
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
|
||||||
generated from metadata of current media.
|
generated from metadata of current media.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plexapi.sync import SyncItem, Policy, MediaSettings
|
from plexapi.sync import SyncItem, Policy, MediaSettings
|
||||||
@@ -109,7 +117,6 @@ class Artist(Audio):
|
|||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Directory'
|
TAG (str): 'Directory'
|
||||||
TYPE (str): 'artist'
|
TYPE (str): 'artist'
|
||||||
art (str): Artist artwork (/library/metadata/<ratingkey>/art/<artid>)
|
|
||||||
countries (list): List of :class:`~plexapi.media.Country` objects this artist respresents.
|
countries (list): List of :class:`~plexapi.media.Country` objects this artist respresents.
|
||||||
genres (list): List of :class:`~plexapi.media.Genre` objects this artist respresents.
|
genres (list): List of :class:`~plexapi.media.Genre` objects this artist respresents.
|
||||||
guid (str): Unknown (unique ID; com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en)
|
guid (str): Unknown (unique ID; com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en)
|
||||||
@@ -123,14 +130,16 @@ class Artist(Audio):
|
|||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
Audio._loadData(self, data)
|
Audio._loadData(self, data)
|
||||||
self.art = data.attrib.get('art')
|
|
||||||
self.guid = data.attrib.get('guid')
|
|
||||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||||
|
self.guid = data.attrib.get('guid')
|
||||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||||
self.countries = self.findItems(data, media.Country)
|
self.countries = self.findItems(data, media.Country)
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
self.genres = self.findItems(data, media.Genre)
|
self.genres = self.findItems(data, media.Genre)
|
||||||
self.similar = self.findItems(data, media.Similar)
|
self.similar = self.findItems(data, media.Similar)
|
||||||
self.collections = self.findItems(data, media.Collection)
|
self.collections = self.findItems(data, media.Collection)
|
||||||
|
self.moods = self.findItems(data, media.Mood)
|
||||||
|
self.styles = self.findItems(data, media.Style)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for album in self.albums():
|
for album in self.albums():
|
||||||
@@ -176,7 +185,7 @@ class Artist(Audio):
|
|||||||
keep_original_name (bool): Set True to keep the original filename as stored in
|
keep_original_name (bool): Set True to keep the original filename as stored in
|
||||||
the Plex server. False will create a new filename with the format
|
the Plex server. False will create a new filename with the format
|
||||||
"<Atrist> - <Album> <Track>".
|
"<Atrist> - <Album> <Track>".
|
||||||
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
|
||||||
be returned and the additional arguments passed in will be sent to that
|
be returned and the additional arguments passed in will be sent to that
|
||||||
function. If kwargs is not specified, the media items will be downloaded
|
function. If kwargs is not specified, the media items will be downloaded
|
||||||
and saved to disk.
|
and saved to disk.
|
||||||
@@ -195,7 +204,6 @@ class Album(Audio):
|
|||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Directory'
|
TAG (str): 'Directory'
|
||||||
TYPE (str): 'album'
|
TYPE (str): 'album'
|
||||||
art (str): Album artwork (/library/metadata/<ratingkey>/art/<artid>)
|
|
||||||
genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents.
|
genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents.
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
originallyAvailableAt (datetime): Datetime this album was released.
|
originallyAvailableAt (datetime): Datetime this album was released.
|
||||||
@@ -216,18 +224,26 @@ class Album(Audio):
|
|||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
Audio._loadData(self, data)
|
Audio._loadData(self, data)
|
||||||
self.art = data.attrib.get('art')
|
self.guid = data.attrib.get('guid')
|
||||||
self.key = self.key.replace('/children', '') # fixes bug #50
|
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||||
|
self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion'))
|
||||||
|
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = data.attrib.get('parentRatingKey')
|
self.parentRatingKey = data.attrib.get('parentRatingKey')
|
||||||
self.parentThumb = data.attrib.get('parentThumb')
|
self.parentThumb = data.attrib.get('parentThumb')
|
||||||
self.parentTitle = data.attrib.get('parentTitle')
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||||
self.studio = data.attrib.get('studio')
|
self.studio = data.attrib.get('studio')
|
||||||
|
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
self.genres = self.findItems(data, media.Genre)
|
|
||||||
self.collections = self.findItems(data, media.Collection)
|
self.collections = self.findItems(data, media.Collection)
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
self.genres = self.findItems(data, media.Genre)
|
||||||
self.labels = self.findItems(data, media.Label)
|
self.labels = self.findItems(data, media.Label)
|
||||||
|
self.moods = self.findItems(data, media.Mood)
|
||||||
|
self.styles = self.findItems(data, media.Style)
|
||||||
|
|
||||||
def track(self, title):
|
def track(self, title):
|
||||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||||
@@ -259,7 +275,7 @@ class Album(Audio):
|
|||||||
keep_original_name (bool): Set True to keep the original filename as stored in
|
keep_original_name (bool): Set True to keep the original filename as stored in
|
||||||
the Plex server. False will create a new filename with the format
|
the Plex server. False will create a new filename with the format
|
||||||
"<Atrist> - <Album> <Track>".
|
"<Atrist> - <Album> <Track>".
|
||||||
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
|
||||||
be returned and the additional arguments passed in will be sent to that
|
be returned and the additional arguments passed in will be sent to that
|
||||||
function. If kwargs is not specified, the media items will be downloaded
|
function. If kwargs is not specified, the media items will be downloaded
|
||||||
and saved to disk.
|
and saved to disk.
|
||||||
@@ -281,7 +297,6 @@ class Track(Audio, Playable):
|
|||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Directory'
|
TAG (str): 'Directory'
|
||||||
TYPE (str): 'track'
|
TYPE (str): 'track'
|
||||||
art (str): Track artwork (/library/metadata/<ratingkey>/art/<artid>)
|
|
||||||
chapterSource (TYPE): Unknown
|
chapterSource (TYPE): Unknown
|
||||||
duration (int): Length of this album in seconds.
|
duration (int): Length of this album in seconds.
|
||||||
grandparentArt (str): Album artist artwork.
|
grandparentArt (str): Album artist artwork.
|
||||||
@@ -316,16 +331,17 @@ class Track(Audio, Playable):
|
|||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
Audio._loadData(self, data)
|
Audio._loadData(self, data)
|
||||||
Playable._loadData(self, data)
|
Playable._loadData(self, data)
|
||||||
self.art = data.attrib.get('art')
|
|
||||||
self.chapterSource = data.attrib.get('chapterSource')
|
self.chapterSource = data.attrib.get('chapterSource')
|
||||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||||
self.grandparentArt = data.attrib.get('grandparentArt')
|
self.grandparentArt = data.attrib.get('grandparentArt')
|
||||||
|
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
||||||
self.grandparentKey = data.attrib.get('grandparentKey')
|
self.grandparentKey = data.attrib.get('grandparentKey')
|
||||||
self.grandparentRatingKey = data.attrib.get('grandparentRatingKey')
|
self.grandparentRatingKey = data.attrib.get('grandparentRatingKey')
|
||||||
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
||||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.originalTitle = data.attrib.get('originalTitle')
|
self.originalTitle = data.attrib.get('originalTitle')
|
||||||
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
self.parentIndex = data.attrib.get('parentIndex')
|
self.parentIndex = data.attrib.get('parentIndex')
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = data.attrib.get('parentRatingKey')
|
self.parentRatingKey = data.attrib.get('parentRatingKey')
|
||||||
@@ -338,6 +354,7 @@ class Track(Audio, Playable):
|
|||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
self.media = self.findItems(data, media.Media)
|
self.media = self.findItems(data, media.Media)
|
||||||
self.moods = self.findItems(data, media.Mood)
|
self.moods = self.findItems(data, media.Mood)
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
|
||||||
def _prettyfilename(self):
|
def _prettyfilename(self):
|
||||||
""" Returns a filename for use in download. """
|
""" Returns a filename for use in download. """
|
||||||
@@ -351,6 +368,13 @@ class Track(Audio, Playable):
|
|||||||
""" Return this track's :class:`~plexapi.audio.Artist`. """
|
""" Return this track's :class:`~plexapi.audio.Artist`. """
|
||||||
return self.fetchItem(self.grandparentKey)
|
return self.fetchItem(self.grandparentKey)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locations(self):
|
||||||
|
""" This does not exist in plex xml response but is added to have a common
|
||||||
|
interface to get the location of the Track
|
||||||
|
"""
|
||||||
|
return [part.file for part in self.iterParts() if part]
|
||||||
|
|
||||||
def _defaultSyncTitle(self):
|
def _defaultSyncTitle(self):
|
||||||
""" Returns str, default title for a new syncItem. """
|
""" Returns str, default title for a new syncItem. """
|
||||||
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)
|
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ class PlexObject(object):
|
|||||||
self._server = server
|
self._server = server
|
||||||
self._data = data
|
self._data = data
|
||||||
self._initpath = initpath or self.key
|
self._initpath = initpath or self.key
|
||||||
self._details_key = ''
|
|
||||||
if data is not None:
|
if data is not None:
|
||||||
self._loadData(data)
|
self._loadData(data)
|
||||||
|
self._details_key = self._buildDetailsKey()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri'))
|
uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri'))
|
||||||
@@ -81,7 +81,7 @@ class PlexObject(object):
|
|||||||
raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, etype))
|
raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, etype))
|
||||||
|
|
||||||
def _buildItemOrNone(self, elem, cls=None, initpath=None):
|
def _buildItemOrNone(self, elem, cls=None, initpath=None):
|
||||||
""" Calls :func:`~plexapi.base.PlexObject._buildItem()` but returns
|
""" Calls :func:`~plexapi.base.PlexObject._buildItem` but returns
|
||||||
None if elem is an unknown type.
|
None if elem is an unknown type.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -89,6 +89,22 @@ class PlexObject(object):
|
|||||||
except UnknownType:
|
except UnknownType:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _buildDetailsKey(self, **kwargs):
|
||||||
|
""" Builds the details key with the XML include parameters.
|
||||||
|
All parameters are included by default with the option to override each parameter
|
||||||
|
or disable each parameter individually by setting it to False or 0.
|
||||||
|
"""
|
||||||
|
details_key = self.key
|
||||||
|
if hasattr(self, '_INCLUDES'):
|
||||||
|
includes = {}
|
||||||
|
for k, v in self._INCLUDES.items():
|
||||||
|
value = kwargs.get(k, v)
|
||||||
|
if value not in [False, 0, '0']:
|
||||||
|
includes[k] = 1 if value is True else value
|
||||||
|
if includes:
|
||||||
|
details_key += '?' + urlencode(sorted(includes.items()))
|
||||||
|
return details_key
|
||||||
|
|
||||||
def fetchItem(self, ekey, cls=None, **kwargs):
|
def fetchItem(self, ekey, cls=None, **kwargs):
|
||||||
""" Load the specified key to find and build the first item with the
|
""" Load the specified key to find and build the first item with the
|
||||||
specified tag and attrs. If no tag or attrs are specified then
|
specified tag and attrs. If no tag or attrs are specified then
|
||||||
@@ -203,9 +219,39 @@ class PlexObject(object):
|
|||||||
results.append(elem.attrib.get(attr))
|
results.append(elem.attrib.get(attr))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def reload(self, key=None):
|
def reload(self, key=None, **kwargs):
|
||||||
""" Reload the data for this object from self.key. """
|
""" Reload the data for this object from self.key.
|
||||||
key = key or self._details_key or self.key
|
|
||||||
|
Parameters:
|
||||||
|
key (string, optional): Override the key to reload.
|
||||||
|
**kwargs (dict): A dictionary of XML include parameters to exclude or override.
|
||||||
|
All parameters are included by default with the option to override each parameter
|
||||||
|
or disable each parameter individually by setting it to False or 0.
|
||||||
|
See :class:`~plexapi.base.PlexPartialObject` for all the available include parameters.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from plexapi.server import PlexServer
|
||||||
|
plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx')
|
||||||
|
movie = plex.library.section('Movies').get('Cars')
|
||||||
|
|
||||||
|
# Partial reload of the movie without the `checkFiles` parameter.
|
||||||
|
# Excluding `checkFiles` will prevent the Plex server from reading the
|
||||||
|
# file to check if the file still exists and is accessible.
|
||||||
|
# The movie object will remain as a partial object.
|
||||||
|
movie.reload(checkFiles=False)
|
||||||
|
movie.isPartialObject() # Returns True
|
||||||
|
|
||||||
|
# Full reload of the movie with all include parameters.
|
||||||
|
# The movie object will be a full object.
|
||||||
|
movie.reload()
|
||||||
|
movie.isFullObject() # Returns True
|
||||||
|
|
||||||
|
"""
|
||||||
|
details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_key
|
||||||
|
key = key or details_key or self.key
|
||||||
if not key:
|
if not key:
|
||||||
raise Unsupported('Cannot reload an object not built from a URL.')
|
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||||
self._initpath = key
|
self._initpath = key
|
||||||
@@ -281,6 +327,27 @@ class PlexPartialObject(PlexObject):
|
|||||||
and if the specified value you request is None it will fetch the full object
|
and if the specified value you request is None it will fetch the full object
|
||||||
automatically and update itself.
|
automatically and update itself.
|
||||||
"""
|
"""
|
||||||
|
_INCLUDES = {
|
||||||
|
'checkFiles': 1,
|
||||||
|
'includeAllConcerts': 1,
|
||||||
|
'includeBandwidths': 1,
|
||||||
|
'includeChapters': 1,
|
||||||
|
'includeChildren': 1,
|
||||||
|
'includeConcerts': 1,
|
||||||
|
'includeExternalMedia': 1,
|
||||||
|
'includeExtras': 1,
|
||||||
|
'includeFields': 'thumbBlurHash,artBlurHash',
|
||||||
|
'includeGeolocation': 1,
|
||||||
|
'includeLoudnessRamps': 1,
|
||||||
|
'includeMarkers': 1,
|
||||||
|
'includeOnDeck': 1,
|
||||||
|
'includePopularLeaves': 1,
|
||||||
|
'includePreferences': 1,
|
||||||
|
'includeRelated': 1,
|
||||||
|
'includeRelatedCount': 1,
|
||||||
|
'includeReviews': 1,
|
||||||
|
'includeStations': 1
|
||||||
|
}
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return other is not None and self.key == other.key
|
return other is not None and self.key == other.key
|
||||||
@@ -332,9 +399,9 @@ class PlexPartialObject(PlexObject):
|
|||||||
""" Retruns True if this is already a full object. A full object means all attributes
|
""" Retruns True if this is already a full object. A full object means all attributes
|
||||||
were populated from the api path representing only this item. For example, the
|
were populated from the api path representing only this item. For example, the
|
||||||
search result for a movie often only contain a portion of the attributes a full
|
search result for a movie often only contain a portion of the attributes a full
|
||||||
object (main url) for that movie contain.
|
object (main url) for that movie would contain.
|
||||||
"""
|
"""
|
||||||
return not self.key or self.key == self._initpath
|
return not self.key or (self._details_key or self.key) == self._initpath
|
||||||
|
|
||||||
def isPartialObject(self):
|
def isPartialObject(self):
|
||||||
""" Returns True if this is not a full object. """
|
""" Returns True if this is not a full object. """
|
||||||
@@ -608,14 +675,6 @@ class Playable(object):
|
|||||||
self.accountID = utils.cast(int, data.attrib.get('accountID')) # history
|
self.accountID = utils.cast(int, data.attrib.get('accountID')) # history
|
||||||
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
|
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
|
||||||
|
|
||||||
def isFullObject(self):
|
|
||||||
""" Retruns True if this is already a full object. A full object means all attributes
|
|
||||||
were populated from the api path representing only this item. For example, the
|
|
||||||
search result for a movie often only contain a portion of the attributes a full
|
|
||||||
object (main url) for that movie contain.
|
|
||||||
"""
|
|
||||||
return self._details_key == self._initpath or not self.key
|
|
||||||
|
|
||||||
def getStreamURL(self, **params):
|
def getStreamURL(self, **params):
|
||||||
""" Returns a stream url that may be used by external applications such as VLC.
|
""" Returns a stream url that may be used by external applications such as VLC.
|
||||||
|
|
||||||
@@ -625,7 +684,7 @@ class Playable(object):
|
|||||||
offset, copyts, protocol, mediaIndex, platform.
|
offset, copyts, protocol, mediaIndex, platform.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL.
|
:exc:`plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL.
|
||||||
"""
|
"""
|
||||||
if self.TYPE not in ('movie', 'episode', 'track'):
|
if self.TYPE not in ('movie', 'episode', 'track'):
|
||||||
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
|
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
|
||||||
@@ -690,7 +749,7 @@ class Playable(object):
|
|||||||
keep_original_name (bool): Set True to keep the original filename as stored in
|
keep_original_name (bool): Set True to keep the original filename as stored in
|
||||||
the Plex server. False will create a new filename with the format
|
the Plex server. False will create a new filename with the format
|
||||||
"<Artist> - <Album> <Track>".
|
"<Artist> - <Album> <Track>".
|
||||||
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
|
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
|
||||||
be returned and the additional arguments passed in will be sent to that
|
be returned and the additional arguments passed in will be sent to that
|
||||||
function. If kwargs is not specified, the media items will be downloaded
|
function. If kwargs is not specified, the media items will be downloaded
|
||||||
and saved to disk.
|
and saved to disk.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class PlexClient(PlexObject):
|
|||||||
_token (str): Token used to access this client.
|
_token (str): Token used to access this client.
|
||||||
_session (obj): Requests session object used to access this client.
|
_session (obj): Requests session object used to access this client.
|
||||||
_proxyThroughServer (bool): Set to True after calling
|
_proxyThroughServer (bool): Set to True after calling
|
||||||
:func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False).
|
:func:`~plexapi.client.PlexClient.proxyThroughServer` (default False).
|
||||||
"""
|
"""
|
||||||
TAG = 'Player'
|
TAG = 'Player'
|
||||||
key = '/resources'
|
key = '/resources'
|
||||||
@@ -138,7 +138,7 @@ class PlexClient(PlexObject):
|
|||||||
value (bool): Enable or disable proxying (optional, default True).
|
value (bool): Enable or disable proxying (optional, default True).
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server.
|
:exc:`plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server.
|
||||||
"""
|
"""
|
||||||
if server:
|
if server:
|
||||||
self._server = server
|
self._server = server
|
||||||
@@ -171,7 +171,7 @@ class PlexClient(PlexObject):
|
|||||||
return ElementTree.fromstring(data) if data.strip() else None
|
return ElementTree.fromstring(data) if data.strip() else None
|
||||||
|
|
||||||
def sendCommand(self, command, proxy=None, **params):
|
def sendCommand(self, command, proxy=None, **params):
|
||||||
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily
|
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query` to more easily
|
||||||
send simple commands to the client. Returns an ElementTree object containing
|
send simple commands to the client. Returns an ElementTree object containing
|
||||||
the response.
|
the response.
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ class PlexClient(PlexObject):
|
|||||||
**params (dict): Additional GET parameters to include with the command.
|
**params (dict): Additional GET parameters to include with the command.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability.
|
:exc:`plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability.
|
||||||
"""
|
"""
|
||||||
command = command.strip('/')
|
command = command.strip('/')
|
||||||
controller = command.split('/')[0]
|
controller = command.split('/')[0]
|
||||||
@@ -296,7 +296,7 @@ class PlexClient(PlexObject):
|
|||||||
**params (dict): Additional GET parameters to include with the command.
|
**params (dict): Additional GET parameters to include with the command.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
:exc:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
||||||
"""
|
"""
|
||||||
if not self._server:
|
if not self._server:
|
||||||
raise Unsupported('A server must be specified before using this command.')
|
raise Unsupported('A server must be specified before using this command.')
|
||||||
@@ -466,7 +466,7 @@ class PlexClient(PlexObject):
|
|||||||
also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands
|
also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
:exc:`plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
||||||
"""
|
"""
|
||||||
if not self._server:
|
if not self._server:
|
||||||
raise Unsupported('A server must be specified before using this command.')
|
raise Unsupported('A server must be specified before using this command.')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
|
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils, media
|
||||||
from plexapi.base import PlexObject
|
from plexapi.base import PlexObject, PlexPartialObject
|
||||||
from plexapi.compat import quote, quote_plus, unquote, urlencode
|
from plexapi.compat import quote, quote_plus, unquote, urlencode
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from plexapi.media import MediaTag
|
from plexapi.media import MediaTag
|
||||||
@@ -455,7 +455,7 @@ class LibrarySection(PlexObject):
|
|||||||
return self.fetchItems(key, **kwargs)
|
return self.fetchItems(key, **kwargs)
|
||||||
|
|
||||||
def agents(self):
|
def agents(self):
|
||||||
""" Returns a list of available `:class:`~plexapi.media.Agent` for this library section.
|
""" Returns a list of available :class:`~plexapi.media.Agent` for this library section.
|
||||||
"""
|
"""
|
||||||
return self._server.agents(utils.searchType(self.type))
|
return self._server.agents(utils.searchType(self.type))
|
||||||
|
|
||||||
@@ -517,7 +517,7 @@ class LibrarySection(PlexObject):
|
|||||||
def listChoices(self, category, libtype=None, **kwargs):
|
def listChoices(self, category, libtype=None, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
|
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
|
||||||
specified category and libtype. kwargs can be any of the same kwargs in
|
specified category and libtype. kwargs can be any of the same kwargs in
|
||||||
:func:`plexapi.library.LibraySection.search()` to help narrow down the choices
|
:func:`~plexapi.library.LibraySection.search` to help narrow down the choices
|
||||||
to only those that matter in your current context.
|
to only those that matter in your current context.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -526,7 +526,7 @@ class LibrarySection(PlexObject):
|
|||||||
**kwargs (dict): Additional kwargs to narrow down the choices.
|
**kwargs (dict): Additional kwargs to narrow down the choices.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
|
:exc:`plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
|
||||||
"""
|
"""
|
||||||
# TODO: Should this be moved to base?
|
# TODO: Should this be moved to base?
|
||||||
if category in kwargs:
|
if category in kwargs:
|
||||||
@@ -573,7 +573,7 @@ class LibrarySection(PlexObject):
|
|||||||
* year: List of years to search within ([yyyy, ...]). [all]
|
* year: List of years to search within ([yyyy, ...]). [all]
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: when applying unknown filter
|
:exc:`plexapi.exceptions.BadRequest`: when applying unknown filter
|
||||||
"""
|
"""
|
||||||
# cleanup the core arguments
|
# cleanup the core arguments
|
||||||
args = {}
|
args = {}
|
||||||
@@ -659,20 +659,20 @@ class LibrarySection(PlexObject):
|
|||||||
def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None,
|
def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
""" Add current library section as sync item for specified device.
|
""" Add current library section as sync item for specified device.
|
||||||
See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting
|
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting
|
||||||
and :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
and :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
policy (:class:`plexapi.sync.Policy`): policy of syncing the media (how many items to sync and process
|
policy (:class:`~plexapi.sync.Policy`): policy of syncing the media (how many items to sync and process
|
||||||
watched media or not), generated automatically when method
|
watched media or not), generated automatically when method
|
||||||
called on specific LibrarySection object.
|
called on specific LibrarySection object.
|
||||||
mediaSettings (:class:`plexapi.sync.MediaSettings`): Transcoding settings used for the media, generated
|
mediaSettings (:class:`~plexapi.sync.MediaSettings`): Transcoding settings used for the media, generated
|
||||||
automatically when method called on specific
|
automatically when method called on specific
|
||||||
LibrarySection object.
|
LibrarySection object.
|
||||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
:func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
|
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
|
||||||
generated from metadata of current media.
|
generated from metadata of current media.
|
||||||
sort (str): formatted as `column:dir`; column can be any of {`addedAt`, `originallyAvailableAt`,
|
sort (str): formatted as `column:dir`; column can be any of {`addedAt`, `originallyAvailableAt`,
|
||||||
`lastViewedAt`, `titleSort`, `rating`, `mediaHeight`, `duration`}. dir can be `asc` or
|
`lastViewedAt`, `titleSort`, `rating`, `mediaHeight`, `duration`}. dir can be `asc` or
|
||||||
@@ -681,10 +681,10 @@ class LibrarySection(PlexObject):
|
|||||||
`track`).
|
`track`).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: when the library is not allowed to sync
|
:exc:`plexapi.exceptions.BadRequest`: when the library is not allowed to sync
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -765,23 +765,36 @@ class MovieSection(LibrarySection):
|
|||||||
METADATA_TYPE = 'movie'
|
METADATA_TYPE = 'movie'
|
||||||
CONTENT_TYPE = 'video'
|
CONTENT_TYPE = 'video'
|
||||||
|
|
||||||
|
def all(self, **kwargs):
|
||||||
|
""" Returns a list of all items from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
|
return self.search(libtype='movie', **kwargs)
|
||||||
|
|
||||||
def collection(self, **kwargs):
|
def collection(self, **kwargs):
|
||||||
""" Returns a list of collections from this library section. """
|
""" Returns a list of collections from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
return self.search(libtype='collection', **kwargs)
|
return self.search(libtype='collection', **kwargs)
|
||||||
|
|
||||||
|
def playlist(self, **kwargs):
|
||||||
|
""" Returns a list of playlists from this library section. """
|
||||||
|
key = '/playlists?type=15&playlistType=%s§ionID=%s' % (self.CONTENT_TYPE, self.key)
|
||||||
|
return self.fetchItems(key, **kwargs)
|
||||||
|
|
||||||
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
|
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
|
||||||
""" Add current Movie library section as sync item for specified device.
|
""" Add current Movie library section as sync item for specified device.
|
||||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
|
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
|
||||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
||||||
:mod:`plexapi.sync` module.
|
:mod:`~plexapi.sync` module.
|
||||||
limit (int): maximum count of movies to sync, unlimited if `None`.
|
limit (int): maximum count of movies to sync, unlimited if `None`.
|
||||||
unwatched (bool): if `True` watched videos wouldn't be synced.
|
unwatched (bool): if `True` watched videos wouldn't be synced.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -830,11 +843,11 @@ class ShowSection(LibrarySection):
|
|||||||
CONTENT_TYPE = 'video'
|
CONTENT_TYPE = 'video'
|
||||||
|
|
||||||
def searchShows(self, **kwargs):
|
def searchShows(self, **kwargs):
|
||||||
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='show', **kwargs)
|
return self.search(libtype='show', **kwargs)
|
||||||
|
|
||||||
def searchEpisodes(self, **kwargs):
|
def searchEpisodes(self, **kwargs):
|
||||||
""" Search for an episode. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for an episode. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='episode', **kwargs)
|
return self.search(libtype='episode', **kwargs)
|
||||||
|
|
||||||
def recentlyAdded(self, libtype='episode', maxresults=50):
|
def recentlyAdded(self, libtype='episode', maxresults=50):
|
||||||
@@ -845,23 +858,36 @@ class ShowSection(LibrarySection):
|
|||||||
"""
|
"""
|
||||||
return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
|
return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
|
||||||
|
|
||||||
|
def all(self, libtype='show', **kwargs):
|
||||||
|
""" Returns a list of all items from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
|
return self.search(libtype=libtype, **kwargs)
|
||||||
|
|
||||||
def collection(self, **kwargs):
|
def collection(self, **kwargs):
|
||||||
""" Returns a list of collections from this library section. """
|
""" Returns a list of collections from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
return self.search(libtype='collection', **kwargs)
|
return self.search(libtype='collection', **kwargs)
|
||||||
|
|
||||||
|
def playlist(self, **kwargs):
|
||||||
|
""" Returns a list of playlists from this library section. """
|
||||||
|
key = '/playlists?type=15&playlistType=%s§ionID=%s' % (self.CONTENT_TYPE, self.key)
|
||||||
|
return self.fetchItems(key, **kwargs)
|
||||||
|
|
||||||
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
|
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
|
||||||
""" Add current Show library section as sync item for specified device.
|
""" Add current Show library section as sync item for specified device.
|
||||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
|
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
|
||||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
||||||
:mod:`plexapi.sync` module.
|
:mod:`~plexapi.sync` module.
|
||||||
limit (int): maximum count of episodes to sync, unlimited if `None`.
|
limit (int): maximum count of episodes to sync, unlimited if `None`.
|
||||||
unwatched (bool): if `True` watched videos wouldn't be synced.
|
unwatched (bool): if `True` watched videos wouldn't be synced.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -915,33 +941,46 @@ class MusicSection(LibrarySection):
|
|||||||
return self.fetchItems(key)
|
return self.fetchItems(key)
|
||||||
|
|
||||||
def searchArtists(self, **kwargs):
|
def searchArtists(self, **kwargs):
|
||||||
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='artist', **kwargs)
|
return self.search(libtype='artist', **kwargs)
|
||||||
|
|
||||||
def searchAlbums(self, **kwargs):
|
def searchAlbums(self, **kwargs):
|
||||||
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='album', **kwargs)
|
return self.search(libtype='album', **kwargs)
|
||||||
|
|
||||||
def searchTracks(self, **kwargs):
|
def searchTracks(self, **kwargs):
|
||||||
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='track', **kwargs)
|
return self.search(libtype='track', **kwargs)
|
||||||
|
|
||||||
|
def all(self, libtype='artist', **kwargs):
|
||||||
|
""" Returns a list of all items from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
|
return self.search(libtype=libtype, **kwargs)
|
||||||
|
|
||||||
def collection(self, **kwargs):
|
def collection(self, **kwargs):
|
||||||
""" Returns a list of collections from this library section. """
|
""" Returns a list of collections from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
return self.search(libtype='collection', **kwargs)
|
return self.search(libtype='collection', **kwargs)
|
||||||
|
|
||||||
|
def playlist(self, **kwargs):
|
||||||
|
""" Returns a list of playlists from this library section. """
|
||||||
|
key = '/playlists?type=15&playlistType=%s§ionID=%s' % (self.CONTENT_TYPE, self.key)
|
||||||
|
return self.fetchItems(key, **kwargs)
|
||||||
|
|
||||||
def sync(self, bitrate, limit=None, **kwargs):
|
def sync(self, bitrate, limit=None, **kwargs):
|
||||||
""" Add current Music library section as sync item for specified device.
|
""" Add current Music library section as sync item for specified device.
|
||||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
|
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
|
||||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
|
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
|
||||||
module :mod:`plexapi.sync`.
|
module :mod:`~plexapi.sync`.
|
||||||
limit (int): maximum count of tracks to sync, unlimited if `None`.
|
limit (int): maximum count of tracks to sync, unlimited if `None`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -984,25 +1023,36 @@ class PhotoSection(LibrarySection):
|
|||||||
METADATA_TYPE = 'photo'
|
METADATA_TYPE = 'photo'
|
||||||
|
|
||||||
def searchAlbums(self, title, **kwargs):
|
def searchAlbums(self, title, **kwargs):
|
||||||
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='photoalbum', title=title, **kwargs)
|
return self.search(libtype='photoalbum', title=title, **kwargs)
|
||||||
|
|
||||||
def searchPhotos(self, title, **kwargs):
|
def searchPhotos(self, title, **kwargs):
|
||||||
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search` for usage. """
|
||||||
return self.search(libtype='photo', title=title, **kwargs)
|
return self.search(libtype='photo', title=title, **kwargs)
|
||||||
|
|
||||||
|
def all(self, libtype='photoalbum', **kwargs):
|
||||||
|
""" Returns a list of all items from this library section.
|
||||||
|
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||||
|
"""
|
||||||
|
return self.search(libtype=libtype, **kwargs)
|
||||||
|
|
||||||
|
def playlist(self, **kwargs):
|
||||||
|
""" Returns a list of playlists from this library section. """
|
||||||
|
key = '/playlists?type=15&playlistType=%s§ionID=%s' % (self.CONTENT_TYPE, self.key)
|
||||||
|
return self.fetchItems(key, **kwargs)
|
||||||
|
|
||||||
def sync(self, resolution, limit=None, **kwargs):
|
def sync(self, resolution, limit=None, **kwargs):
|
||||||
""" Add current Music library section as sync item for specified device.
|
""" Add current Music library section as sync item for specified device.
|
||||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
|
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting and
|
||||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
:func:`~plexapi.library.LibrarySection.sync` for details on syncing libraries and possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
|
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
|
||||||
module :mod:`plexapi.sync`.
|
module :mod:`~plexapi.sync`.
|
||||||
limit (int): maximum count of tracks to sync, unlimited if `None`.
|
limit (int): maximum count of tracks to sync, unlimited if `None`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -1029,7 +1079,7 @@ class PhotoSection(LibrarySection):
|
|||||||
class FilterChoice(PlexObject):
|
class FilterChoice(PlexObject):
|
||||||
""" Represents a single filter choice. These objects are gathered when using filters
|
""" Represents a single filter choice. These objects are gathered when using filters
|
||||||
while searching for library items and is the object returned in the result set of
|
while searching for library items and is the object returned in the result set of
|
||||||
:func:`~plexapi.library.LibrarySection.listChoices()`.
|
:func:`~plexapi.library.LibrarySection.listChoices`.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Directory'
|
TAG (str): 'Directory'
|
||||||
@@ -1083,33 +1133,84 @@ class Hub(PlexObject):
|
|||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Collections(PlexObject):
|
class Collections(PlexPartialObject):
|
||||||
|
""" Represents a single Collection.
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Directory'
|
||||||
|
TYPE (str): 'collection'
|
||||||
|
ratingKey (int): Unique key identifying this item.
|
||||||
|
addedAt (datetime): Datetime this item was added to the library.
|
||||||
|
art (str): URL to artwork image.
|
||||||
|
artBlurHash (str): BlurHash string for artwork image.
|
||||||
|
childCount (int): Count of child object(s)
|
||||||
|
collectionMode (str): How the items in the collection are displayed.
|
||||||
|
collectionSort (str): How to sort the items in the collection.
|
||||||
|
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||||
|
fields (list): List of :class:`~plexapi.media.Field`.
|
||||||
|
guid (str): Plex GUID (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
|
||||||
|
index (int): Unknown
|
||||||
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
|
labels (List<:class:`~plexapi.media.Label`>): List of field objects.
|
||||||
|
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||||
|
librarySectionKey (str): API URL (/library/sections/<sectionkey>).
|
||||||
|
librarySectionTitle (str): Section Title
|
||||||
|
maxYear (int): Year
|
||||||
|
minYear (int): Year
|
||||||
|
subtype (str): Media type
|
||||||
|
summary (str): Summary of the collection
|
||||||
|
thumb (str): URL to thumbnail image.
|
||||||
|
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||||
|
title (str): Collection Title
|
||||||
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
|
type (str): Hardcoded 'collection'
|
||||||
|
updatedAt (datatime): Datetime this item was updated.
|
||||||
|
"""
|
||||||
|
|
||||||
TAG = 'Directory'
|
TAG = 'Directory'
|
||||||
TYPE = 'collection'
|
TYPE = 'collection'
|
||||||
_include = "?includeExternalMedia=1&includePreferences=1"
|
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||||
self._details_key = "/library/metadata/%s%s" % (self.ratingKey, self._include)
|
self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50
|
||||||
self.key = data.attrib.get('key')
|
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||||
self.type = data.attrib.get('type')
|
self.art = data.attrib.get('art')
|
||||||
self.title = data.attrib.get('title')
|
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||||
|
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||||
|
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode'))
|
||||||
|
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort'))
|
||||||
|
self.contentRating = data.attrib.get('contentRating')
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
self.guid = data.attrib.get('guid')
|
||||||
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
|
self.labels = self.findItems(data, media.Label)
|
||||||
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||||
|
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
|
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
||||||
|
self.minYear = utils.cast(int, data.attrib.get('minYear'))
|
||||||
self.subtype = data.attrib.get('subtype')
|
self.subtype = data.attrib.get('subtype')
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
|
self.titleSort = data.attrib.get('titleSort')
|
||||||
|
self.type = data.attrib.get('type')
|
||||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
|
||||||
self.minYear = utils.cast(int, data.attrib.get('minYear'))
|
def children(self):
|
||||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
""" Returns a list of all items in the collection. """
|
||||||
self.collectionMode = data.attrib.get('collectionMode')
|
key = '/library/metadata/%s/children' % self.ratingKey
|
||||||
self.collectionSort = data.attrib.get('collectionSort')
|
return self.fetchItems(key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self):
|
def thumbUrl(self):
|
||||||
return self.fetchItems(self.key)
|
""" Return the thumbnail url for the collection."""
|
||||||
|
return self._server.url(self.thumb, includeToken=True) if self.thumb else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def artUrl(self):
|
||||||
|
""" Return the art url for the collection."""
|
||||||
|
return self._server.url(self.art, includeToken=True) if self.art else None
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self.childCount
|
return self.childCount
|
||||||
@@ -1120,18 +1221,16 @@ class Collections(PlexObject):
|
|||||||
|
|
||||||
def modeUpdate(self, mode=None):
|
def modeUpdate(self, mode=None):
|
||||||
""" Update Collection Mode
|
""" Update Collection Mode
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
mode: default (Library default)
|
mode: default (Library default)
|
||||||
hide (Hide Collection)
|
hide (Hide Collection)
|
||||||
hideItems (Hide Items in this Collection)
|
hideItems (Hide Items in this Collection)
|
||||||
showItems (Show this Collection and its Items)
|
showItems (Show this Collection and its Items)
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
collection = 'plexapi.library.Collections'
|
collection = 'plexapi.library.Collections'
|
||||||
collection.updateMode(mode="hide")
|
collection.updateMode(mode="hide")
|
||||||
"""
|
"""
|
||||||
mode_dict = {'default': '-2',
|
mode_dict = {'default': '-1',
|
||||||
'hide': '0',
|
'hide': '0',
|
||||||
'hideItems': '1',
|
'hideItems': '1',
|
||||||
'showItems': '2'}
|
'showItems': '2'}
|
||||||
@@ -1143,13 +1242,10 @@ class Collections(PlexObject):
|
|||||||
|
|
||||||
def sortUpdate(self, sort=None):
|
def sortUpdate(self, sort=None):
|
||||||
""" Update Collection Sorting
|
""" Update Collection Sorting
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
sort: realease (Order Collection by realease dates)
|
sort: realease (Order Collection by realease dates)
|
||||||
alpha (Order Collection Alphabetically)
|
alpha (Order Collection Alphabetically)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
colleciton = 'plexapi.library.Collections'
|
colleciton = 'plexapi.library.Collections'
|
||||||
collection.updateSort(mode="alpha")
|
collection.updateSort(mode="alpha")
|
||||||
"""
|
"""
|
||||||
@@ -1201,3 +1297,54 @@ class Collections(PlexObject):
|
|||||||
|
|
||||||
# def edit(self, **kwargs):
|
# def edit(self, **kwargs):
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Path(PlexObject):
|
||||||
|
""" Represents a single directory Path.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Path'
|
||||||
|
|
||||||
|
home (bool): True if the path is the home directory
|
||||||
|
key (str): API URL (/services/browse/<base64path>)
|
||||||
|
network (bool): True if path is a network location
|
||||||
|
path (str): Full path to folder
|
||||||
|
title (str): Folder name
|
||||||
|
"""
|
||||||
|
TAG = 'Path'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||||
|
self.key = data.attrib.get('key')
|
||||||
|
self.network = utils.cast(bool, data.attrib.get('network'))
|
||||||
|
self.path = data.attrib.get('path')
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
|
|
||||||
|
def browse(self, includeFiles=True):
|
||||||
|
""" Alias for :func:`~plexapi.server.PlexServer.browse`. """
|
||||||
|
return self._server.browse(self, includeFiles)
|
||||||
|
|
||||||
|
def walk(self):
|
||||||
|
""" Alias for :func:`~plexapi.server.PlexServer.walk`. """
|
||||||
|
for path, paths, files in self._server.walk(self):
|
||||||
|
yield path, paths, files
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class File(PlexObject):
|
||||||
|
""" Represents a single File.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'File'
|
||||||
|
|
||||||
|
key (str): API URL (/services/browse/<base64path>)
|
||||||
|
path (str): Full path to file
|
||||||
|
title (str): File name
|
||||||
|
"""
|
||||||
|
TAG = 'File'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self.key = data.attrib.get('key')
|
||||||
|
self.path = data.attrib.get('path')
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import xml
|
|||||||
from plexapi import compat, log, settings, utils
|
from plexapi import compat, log, settings, utils
|
||||||
from plexapi.base import PlexObject
|
from plexapi.base import PlexObject
|
||||||
from plexapi.exceptions import BadRequest
|
from plexapi.exceptions import BadRequest
|
||||||
from plexapi.utils import cast
|
from plexapi.utils import cast, SEARCHTYPES
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
@@ -45,6 +45,7 @@ class Media(PlexObject):
|
|||||||
self.aspectRatio = cast(float, data.attrib.get('aspectRatio'))
|
self.aspectRatio = cast(float, data.attrib.get('aspectRatio'))
|
||||||
self.audioChannels = cast(int, data.attrib.get('audioChannels'))
|
self.audioChannels = cast(int, data.attrib.get('audioChannels'))
|
||||||
self.audioCodec = data.attrib.get('audioCodec')
|
self.audioCodec = data.attrib.get('audioCodec')
|
||||||
|
self.audioProfile = data.attrib.get('videoProfile')
|
||||||
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
||||||
self.container = data.attrib.get('container')
|
self.container = data.attrib.get('container')
|
||||||
self.duration = cast(int, data.attrib.get('duration'))
|
self.duration = cast(int, data.attrib.get('duration'))
|
||||||
@@ -60,6 +61,16 @@ class Media(PlexObject):
|
|||||||
self.videoResolution = data.attrib.get('videoResolution')
|
self.videoResolution = data.attrib.get('videoResolution')
|
||||||
self.width = cast(int, data.attrib.get('width'))
|
self.width = cast(int, data.attrib.get('width'))
|
||||||
self.parts = self.findItems(data, MediaPart)
|
self.parts = self.findItems(data, MediaPart)
|
||||||
|
self.proxyType = cast(int, data.attrib.get('proxyType'))
|
||||||
|
self.optimizedVersion = self.proxyType == SEARCHTYPES['optimizedVersion']
|
||||||
|
|
||||||
|
# For Photo only
|
||||||
|
self.aperture = data.attrib.get('aperture')
|
||||||
|
self.exposure = data.attrib.get('exposure')
|
||||||
|
self.iso = cast(int, data.attrib.get('iso'))
|
||||||
|
self.lens = data.attrib.get('lens')
|
||||||
|
self.make = data.attrib.get('make')
|
||||||
|
self.model = data.attrib.get('model')
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
part = self._initpath + '/media/%s' % self.id
|
part = self._initpath + '/media/%s' % self.id
|
||||||
@@ -96,15 +107,21 @@ class MediaPart(PlexObject):
|
|||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
self._data = data
|
self._data = data
|
||||||
|
self.audioProfile = data.attrib.get('audioProfile')
|
||||||
self.container = data.attrib.get('container')
|
self.container = data.attrib.get('container')
|
||||||
|
self.deepAnalysisVersion = cast(int, data.attrib.get('deepAnalysisVersion'))
|
||||||
self.duration = cast(int, data.attrib.get('duration'))
|
self.duration = cast(int, data.attrib.get('duration'))
|
||||||
self.file = data.attrib.get('file')
|
self.file = data.attrib.get('file')
|
||||||
|
self.has64bitOffsets = cast(bool, data.attrib.get('has64bitOffsets'))
|
||||||
|
self.hasThumbnail = cast(bool, data.attrib.get('hasThumbnail'))
|
||||||
self.id = cast(int, data.attrib.get('id'))
|
self.id = cast(int, data.attrib.get('id'))
|
||||||
self.indexes = data.attrib.get('indexes')
|
self.indexes = data.attrib.get('indexes')
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.size = cast(int, data.attrib.get('size'))
|
self.size = cast(int, data.attrib.get('size'))
|
||||||
self.decision = data.attrib.get('decision')
|
self.decision = data.attrib.get('decision')
|
||||||
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming'))
|
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming'))
|
||||||
|
self.packetLength = cast(int, data.attrib.get('packetLength'))
|
||||||
|
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
|
||||||
self.syncItemId = cast(int, data.attrib.get('syncItemId'))
|
self.syncItemId = cast(int, data.attrib.get('syncItemId'))
|
||||||
self.syncState = data.attrib.get('syncState')
|
self.syncState = data.attrib.get('syncState')
|
||||||
self.videoProfile = data.attrib.get('videoProfile')
|
self.videoProfile = data.attrib.get('videoProfile')
|
||||||
@@ -112,10 +129,13 @@ class MediaPart(PlexObject):
|
|||||||
self.exists = cast(bool, data.attrib.get('exists'))
|
self.exists = cast(bool, data.attrib.get('exists'))
|
||||||
self.accessible = cast(bool, data.attrib.get('accessible'))
|
self.accessible = cast(bool, data.attrib.get('accessible'))
|
||||||
|
|
||||||
|
# For Photo only
|
||||||
|
self.orientation = cast(int, data.attrib.get('orientation'))
|
||||||
|
|
||||||
def _buildStreams(self, data):
|
def _buildStreams(self, data):
|
||||||
streams = []
|
streams = []
|
||||||
for elem in data:
|
for elem in data:
|
||||||
for cls in (VideoStream, AudioStream, SubtitleStream):
|
for cls in (VideoStream, AudioStream, SubtitleStream, LyricStream):
|
||||||
if elem.attrib.get('streamType') == str(cls.STREAMTYPE):
|
if elem.attrib.get('streamType') == str(cls.STREAMTYPE):
|
||||||
streams.append(cls(self._server, elem, self._initpath))
|
streams.append(cls(self._server, elem, self._initpath))
|
||||||
return streams
|
return streams
|
||||||
@@ -132,6 +152,10 @@ class MediaPart(PlexObject):
|
|||||||
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
|
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """
|
||||||
return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE]
|
return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE]
|
||||||
|
|
||||||
|
def lyricStreams(self):
|
||||||
|
""" Returns a list of :class:`~plexapi.media.LyricStream` objects in this MediaPart. """
|
||||||
|
return [stream for stream in self.streams if stream.streamType == LyricStream.STREAMTYPE]
|
||||||
|
|
||||||
def setDefaultAudioStream(self, stream):
|
def setDefaultAudioStream(self, stream):
|
||||||
""" Set the default :class:`~plexapi.media.AudioStream` for this MediaPart.
|
""" Set the default :class:`~plexapi.media.AudioStream` for this MediaPart.
|
||||||
|
|
||||||
@@ -177,7 +201,8 @@ class MediaPartStream(PlexObject):
|
|||||||
languageCode (str): Ascii code for language (ex: eng, tha).
|
languageCode (str): Ascii code for language (ex: eng, tha).
|
||||||
selected (bool): True if this stream is selected.
|
selected (bool): True if this stream is selected.
|
||||||
streamType (int): Stream type (1=:class:`~plexapi.media.VideoStream`,
|
streamType (int): Stream type (1=:class:`~plexapi.media.VideoStream`,
|
||||||
2=:class:`~plexapi.media.AudioStream`, 3=:class:`~plexapi.media.SubtitleStream`).
|
2=:class:`~plexapi.media.AudioStream`, 3=:class:`~plexapi.media.SubtitleStream`,
|
||||||
|
4=:class:`~plexapi.media.LyricStream`).
|
||||||
type (int): Alias for streamType.
|
type (int): Alias for streamType.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -185,19 +210,22 @@ class MediaPartStream(PlexObject):
|
|||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
self._data = data
|
self._data = data
|
||||||
self.codec = data.attrib.get('codec')
|
self.codec = data.attrib.get('codec')
|
||||||
self.codecID = data.attrib.get('codecID')
|
self.default = cast(bool, data.attrib.get('selected', '0'))
|
||||||
|
self.displayTitle = data.attrib.get('displayTitle')
|
||||||
|
self.extendedDisplayTitle = data.attrib.get('extendedDisplayTitle')
|
||||||
self.id = cast(int, data.attrib.get('id'))
|
self.id = cast(int, data.attrib.get('id'))
|
||||||
self.index = cast(int, data.attrib.get('index', '-1'))
|
self.index = cast(int, data.attrib.get('index', '-1'))
|
||||||
self.language = data.attrib.get('language')
|
self.language = data.attrib.get('language')
|
||||||
self.languageCode = data.attrib.get('languageCode')
|
self.languageCode = data.attrib.get('languageCode')
|
||||||
self.selected = cast(bool, data.attrib.get('selected', '0'))
|
self.selected = cast(bool, data.attrib.get('selected', '0'))
|
||||||
self.streamType = cast(int, data.attrib.get('streamType'))
|
self.streamType = cast(int, data.attrib.get('streamType'))
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
self.type = cast(int, data.attrib.get('streamType'))
|
self.type = cast(int, data.attrib.get('streamType'))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(server, data, initpath): # pragma: no cover seems to be dead code.
|
def parse(server, data, initpath): # pragma: no cover seems to be dead code.
|
||||||
""" Factory method returns a new MediaPartStream from xml data. """
|
""" Factory method returns a new MediaPartStream from xml data. """
|
||||||
STREAMCLS = {1: VideoStream, 2: AudioStream, 3: SubtitleStream}
|
STREAMCLS = {1: VideoStream, 2: AudioStream, 3: SubtitleStream, 4: LyricStream}
|
||||||
stype = cast(int, data.attrib.get('streamType'))
|
stype = cast(int, data.attrib.get('streamType'))
|
||||||
cls = STREAMCLS.get(stype, MediaPartStream)
|
cls = STREAMCLS.get(stype, MediaPartStream)
|
||||||
return cls(server, data, initpath)
|
return cls(server, data, initpath)
|
||||||
@@ -233,21 +261,40 @@ class VideoStream(MediaPartStream):
|
|||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
super(VideoStream, self)._loadData(data)
|
super(VideoStream, self)._loadData(data)
|
||||||
|
self.anamorphic = data.attrib.get('anamorphic')
|
||||||
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
|
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
|
||||||
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
||||||
self.cabac = cast(int, data.attrib.get('cabac'))
|
self.cabac = cast(int, data.attrib.get('cabac'))
|
||||||
|
self.chromaLocation = data.attrib.get('chromaLocation')
|
||||||
self.chromaSubsampling = data.attrib.get('chromaSubsampling')
|
self.chromaSubsampling = data.attrib.get('chromaSubsampling')
|
||||||
|
self.codecID = data.attrib.get('codecID')
|
||||||
|
self.codedHeight = data.attrib.get('codedHeight')
|
||||||
|
self.codedWidth = data.attrib.get('codedWidth')
|
||||||
|
self.colorPrimaries = data.attrib.get('colorPrimaries')
|
||||||
|
self.colorRange = data.attrib.get('colorRange')
|
||||||
self.colorSpace = data.attrib.get('colorSpace')
|
self.colorSpace = data.attrib.get('colorSpace')
|
||||||
|
self.colorTrc = data.attrib.get('colorTrc')
|
||||||
|
self.DOVIBLCompatID = cast(int, data.attrib.get('DOVIBLCompatID'))
|
||||||
|
self.DOVIBLPresent = cast(bool, data.attrib.get('DOVIBLPresent'))
|
||||||
|
self.DOVIELPresent = cast(bool, data.attrib.get('DOVIELPresent'))
|
||||||
|
self.DOVILevel = cast(int, data.attrib.get('DOVILevel'))
|
||||||
|
self.DOVIPresent = cast(bool, data.attrib.get('DOVIPresent'))
|
||||||
|
self.DOVIProfile = cast(int, data.attrib.get('DOVIProfile'))
|
||||||
|
self.DOVIRPUPresent = cast(bool, data.attrib.get('DOVIRPUPresent'))
|
||||||
|
self.DOVIVersion = cast(float, data.attrib.get('DOVIVersion'))
|
||||||
self.duration = cast(int, data.attrib.get('duration'))
|
self.duration = cast(int, data.attrib.get('duration'))
|
||||||
self.frameRate = cast(float, data.attrib.get('frameRate'))
|
self.frameRate = cast(float, data.attrib.get('frameRate'))
|
||||||
self.frameRateMode = data.attrib.get('frameRateMode')
|
self.frameRateMode = data.attrib.get('frameRateMode')
|
||||||
self.hasScallingMatrix = cast(bool, data.attrib.get('hasScallingMatrix'))
|
self.hasScalingMatrix = cast(bool, data.attrib.get('hasScalingMatrix'))
|
||||||
self.height = cast(int, data.attrib.get('height'))
|
self.height = cast(int, data.attrib.get('height'))
|
||||||
self.level = cast(int, data.attrib.get('level'))
|
self.level = cast(int, data.attrib.get('level'))
|
||||||
self.profile = data.attrib.get('profile')
|
self.profile = data.attrib.get('profile')
|
||||||
self.refFrames = cast(int, data.attrib.get('refFrames'))
|
self.refFrames = cast(int, data.attrib.get('refFrames'))
|
||||||
|
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
|
||||||
|
self.pixelAspectRatio = data.attrib.get('pixelAspectRatio')
|
||||||
|
self.pixelFormat = data.attrib.get('pixelFormat')
|
||||||
self.scanType = data.attrib.get('scanType')
|
self.scanType = data.attrib.get('scanType')
|
||||||
self.title = data.attrib.get('title')
|
self.streamIdentifier = cast(int, data.attrib.get('streamIdentifier'))
|
||||||
self.width = cast(int, data.attrib.get('width'))
|
self.width = cast(int, data.attrib.get('width'))
|
||||||
|
|
||||||
|
|
||||||
@@ -279,10 +326,22 @@ class AudioStream(MediaPartStream):
|
|||||||
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
||||||
self.bitrateMode = data.attrib.get('bitrateMode')
|
self.bitrateMode = data.attrib.get('bitrateMode')
|
||||||
self.channels = cast(int, data.attrib.get('channels'))
|
self.channels = cast(int, data.attrib.get('channels'))
|
||||||
self.dialogNorm = cast(int, data.attrib.get('dialogNorm'))
|
|
||||||
self.duration = cast(int, data.attrib.get('duration'))
|
self.duration = cast(int, data.attrib.get('duration'))
|
||||||
|
self.profile = data.attrib.get('profile')
|
||||||
|
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
|
||||||
self.samplingRate = cast(int, data.attrib.get('samplingRate'))
|
self.samplingRate = cast(int, data.attrib.get('samplingRate'))
|
||||||
self.title = data.attrib.get('title')
|
self.streamIdentifier = cast(int, data.attrib.get('streamIdentifier'))
|
||||||
|
|
||||||
|
# For Track only
|
||||||
|
self.albumGain = cast(float, data.attrib.get('albumGain'))
|
||||||
|
self.albumPeak = cast(float, data.attrib.get('albumPeak'))
|
||||||
|
self.albumRange = cast(float, data.attrib.get('albumRange'))
|
||||||
|
self.endRamp = data.attrib.get('endRamp')
|
||||||
|
self.gain = cast(float, data.attrib.get('gain'))
|
||||||
|
self.loudness = cast(float, data.attrib.get('loudness'))
|
||||||
|
self.lra = cast(float, data.attrib.get('lra'))
|
||||||
|
self.peak = cast(float, data.attrib.get('peak'))
|
||||||
|
self.startRamp = data.attrib.get('startRamp')
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
@@ -303,10 +362,37 @@ class SubtitleStream(MediaPartStream):
|
|||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
super(SubtitleStream, self)._loadData(data)
|
super(SubtitleStream, self)._loadData(data)
|
||||||
|
self.container = data.attrib.get('container')
|
||||||
self.forced = cast(bool, data.attrib.get('forced', '0'))
|
self.forced = cast(bool, data.attrib.get('forced', '0'))
|
||||||
self.format = data.attrib.get('format')
|
self.format = data.attrib.get('format')
|
||||||
|
self.headerCompression = data.attrib.get('headerCompression')
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.title = data.attrib.get('title')
|
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
|
||||||
|
self.transient = data.attrib.get('transient')
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class LyricStream(MediaPartStream):
|
||||||
|
""" Respresents a lyric stream within a :class:`~plexapi.media.MediaPart`.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Stream'
|
||||||
|
STREAMTYPE (int): 4
|
||||||
|
format (str): Lyric format (ex: lrc).
|
||||||
|
key (str): Key of this subtitle stream (ex: /library/streams/212284).
|
||||||
|
title (str): Title of this lyric stream.
|
||||||
|
"""
|
||||||
|
TAG = 'Stream'
|
||||||
|
STREAMTYPE = 4
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
""" Load attribute values from Plex XML response. """
|
||||||
|
super(LyricStream, self)._loadData(data)
|
||||||
|
self.format = data.attrib.get('format')
|
||||||
|
self.key = data.attrib.get('key')
|
||||||
|
self.minLines = cast(int, data.attrib.get('minLines'))
|
||||||
|
self.provider = data.attrib.get('provider')
|
||||||
|
self.timed = cast(bool, data.attrib.get('timed', '0'))
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
@@ -476,7 +562,7 @@ class MediaTag(PlexObject):
|
|||||||
tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of
|
tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of
|
||||||
person for Directors and Roles (ex: Animation, Stephen Graham, etc).
|
person for Directors and Roles (ex: Animation, Stephen Graham, etc).
|
||||||
<Hub_Search_Attributes>: Attributes only applicable in search results from
|
<Hub_Search_Attributes>: Attributes only applicable in search results from
|
||||||
PlexServer :func:`~plexapi.server.PlexServer.search()`. They provide details of which
|
PlexServer :func:`~plexapi.server.PlexServer.search`. They provide details of which
|
||||||
library section the tag was found as well as the url to dig deeper into the results.
|
library section the tag was found as well as the url to dig deeper into the results.
|
||||||
|
|
||||||
* key (str): API URL to dig deeper into this tag (ex: /library/sections/1/all?actor=9081).
|
* key (str): API URL to dig deeper into this tag (ex: /library/sections/1/all?actor=9081).
|
||||||
@@ -501,6 +587,29 @@ class MediaTag(PlexObject):
|
|||||||
self.tagType = cast(int, data.attrib.get('tagType'))
|
self.tagType = cast(int, data.attrib.get('tagType'))
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
|
||||||
|
def items(self, *args, **kwargs):
|
||||||
|
""" Return the list of items within this tag. This function is only applicable
|
||||||
|
in search results from PlexServer :func:`~plexapi.server.PlexServer.search`.
|
||||||
|
"""
|
||||||
|
if not self.key:
|
||||||
|
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
|
||||||
|
return self.fetchItems(self.key)
|
||||||
|
|
||||||
|
|
||||||
|
class GuidTag(PlexObject):
|
||||||
|
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
||||||
|
Attributes:
|
||||||
|
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
||||||
|
id (id): Tag ID (Used as a unique id, except for Guid's, used for external systems
|
||||||
|
to plex identifiers, like imdb and tmdb).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
""" Load attribute values from Plex XML response. """
|
||||||
|
self._data = data
|
||||||
|
self.id = data.attrib.get('id')
|
||||||
|
self.tag = data.attrib.get('tag')
|
||||||
|
|
||||||
def items(self, *args, **kwargs):
|
def items(self, *args, **kwargs):
|
||||||
""" Return the list of items within this tag. This function is only applicable
|
""" Return the list of items within this tag. This function is only applicable
|
||||||
in search results from PlexServer :func:`~plexapi.server.PlexServer.search()`.
|
in search results from PlexServer :func:`~plexapi.server.PlexServer.search()`.
|
||||||
@@ -589,6 +698,12 @@ class Genre(MediaTag):
|
|||||||
FILTER = 'genre'
|
FILTER = 'genre'
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Guid(GuidTag):
|
||||||
|
""" Represents a single Guid media tag. """
|
||||||
|
TAG = "Guid"
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Mood(MediaTag):
|
class Mood(MediaTag):
|
||||||
""" Represents a single Mood media tag.
|
""" Represents a single Mood media tag.
|
||||||
@@ -601,6 +716,18 @@ class Mood(MediaTag):
|
|||||||
FILTER = 'mood'
|
FILTER = 'mood'
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Style(MediaTag):
|
||||||
|
""" Represents a single Style media tag.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Style'
|
||||||
|
FILTER (str): 'style'
|
||||||
|
"""
|
||||||
|
TAG = 'Style'
|
||||||
|
FILTER = 'style'
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Poster(PlexObject):
|
class Poster(PlexObject):
|
||||||
""" Represents a Poster.
|
""" Represents a Poster.
|
||||||
@@ -614,7 +741,7 @@ class Poster(PlexObject):
|
|||||||
self._data = data
|
self._data = data
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.ratingKey = data.attrib.get('ratingKey')
|
self.ratingKey = data.attrib.get('ratingKey')
|
||||||
self.selected = data.attrib.get('selected')
|
self.selected = cast(bool, data.attrib.get('selected'))
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
|
||||||
def select(self):
|
def select(self):
|
||||||
@@ -689,11 +816,33 @@ class Chapter(PlexObject):
|
|||||||
self.filter = data.attrib.get('filter') # I couldn't filter on it anyways
|
self.filter = data.attrib.get('filter') # I couldn't filter on it anyways
|
||||||
self.tag = data.attrib.get('tag')
|
self.tag = data.attrib.get('tag')
|
||||||
self.title = self.tag
|
self.title = self.tag
|
||||||
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.index = cast(int, data.attrib.get('index'))
|
self.index = cast(int, data.attrib.get('index'))
|
||||||
self.start = cast(int, data.attrib.get('startTimeOffset'))
|
self.start = cast(int, data.attrib.get('startTimeOffset'))
|
||||||
self.end = cast(int, data.attrib.get('endTimeOffset'))
|
self.end = cast(int, data.attrib.get('endTimeOffset'))
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Marker(PlexObject):
|
||||||
|
""" Represents a single Marker media tag.
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Marker'
|
||||||
|
"""
|
||||||
|
TAG = 'Marker'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
name = self._clean(self.firstAttr('type'))
|
||||||
|
start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start')))
|
||||||
|
end = utils.millisecondToHumanstr(self._clean(self.firstAttr('end')))
|
||||||
|
return '<%s:%s %s - %s>' % (self.__class__.__name__, name, start, end)
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self._data = data
|
||||||
|
self.type = data.attrib.get('type')
|
||||||
|
self.start = cast(int, data.attrib.get('startTimeOffset'))
|
||||||
|
self.end = cast(int, data.attrib.get('endTimeOffset'))
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Field(PlexObject):
|
class Field(PlexObject):
|
||||||
""" Represents a single Field.
|
""" Represents a single Field.
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ class MyPlexAccount(PlexObject):
|
|||||||
return self.query(url, method=self._session.put, data=params)
|
return self.query(url, method=self._session.put, data=params)
|
||||||
|
|
||||||
def syncItems(self, client=None, clientId=None):
|
def syncItems(self, client=None, clientId=None):
|
||||||
""" Returns an instance of :class:`plexapi.sync.SyncList` for specified client.
|
""" Returns an instance of :class:`~plexapi.sync.SyncList` for specified client.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
client (:class:`~plexapi.myplex.MyPlexDevice`): a client to query SyncItems for.
|
client (:class:`~plexapi.myplex.MyPlexDevice`): a client to query SyncItems for.
|
||||||
@@ -564,22 +564,22 @@ class MyPlexAccount(PlexObject):
|
|||||||
|
|
||||||
def sync(self, sync_item, client=None, clientId=None):
|
def sync(self, sync_item, client=None, clientId=None):
|
||||||
""" Adds specified sync item for the client. It's always easier to use methods defined directly in the media
|
""" Adds specified sync item for the client. It's always easier to use methods defined directly in the media
|
||||||
objects, e.g. :func:`plexapi.video.Video.sync`, :func:`plexapi.audio.Audio.sync`.
|
objects, e.g. :func:`~plexapi.video.Video.sync`, :func:`~plexapi.audio.Audio.sync`.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
client (:class:`~plexapi.myplex.MyPlexDevice`): a client for which you need to add SyncItem to.
|
client (:class:`~plexapi.myplex.MyPlexDevice`): a client for which you need to add SyncItem to.
|
||||||
clientId (str): an identifier of a client for which you need to add SyncItem to.
|
clientId (str): an identifier of a client for which you need to add SyncItem to.
|
||||||
sync_item (:class:`plexapi.sync.SyncItem`): prepared SyncItem object with all fields set.
|
sync_item (:class:`~plexapi.sync.SyncItem`): prepared SyncItem object with all fields set.
|
||||||
|
|
||||||
If both `client` and `clientId` provided the client would be preferred.
|
If both `client` and `clientId` provided the client would be preferred.
|
||||||
If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier.
|
If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: when client with provided clientId wasn`t found.
|
:exc:`plexapi.exceptions.BadRequest`: when client with provided clientId wasn`t found.
|
||||||
:class:`plexapi.exceptions.BadRequest`: provided client doesn`t provides `sync-target`.
|
:exc:`plexapi.exceptions.BadRequest`: provided client doesn`t provides `sync-target`.
|
||||||
"""
|
"""
|
||||||
if not client and not clientId:
|
if not client and not clientId:
|
||||||
clientId = X_PLEX_IDENTIFIER
|
clientId = X_PLEX_IDENTIFIER
|
||||||
@@ -686,7 +686,7 @@ class MyPlexAccount(PlexObject):
|
|||||||
|
|
||||||
class MyPlexUser(PlexObject):
|
class MyPlexUser(PlexObject):
|
||||||
""" This object represents non-signed in users such as friends and linked
|
""" This object represents non-signed in users such as friends and linked
|
||||||
accounts. NOTE: This should not be confused with the :class:`~myplex.MyPlexAccount`
|
accounts. NOTE: This should not be confused with the :class:`~plexapi.myplex.MyPlexAccount`
|
||||||
which is your specific account. The raw xml for the data presented here
|
which is your specific account. The raw xml for the data presented here
|
||||||
can be found at: https://plex.tv/api/users/
|
can be found at: https://plex.tv/api/users/
|
||||||
|
|
||||||
@@ -885,7 +885,7 @@ class MyPlexResource(PlexObject):
|
|||||||
key (str): 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1'
|
key (str): 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1'
|
||||||
accessToken (str): This resources accesstoken.
|
accessToken (str): This resources accesstoken.
|
||||||
clientIdentifier (str): Unique ID for this resource.
|
clientIdentifier (str): Unique ID for this resource.
|
||||||
connections (list): List of :class:`~myplex.ResourceConnection` objects
|
connections (list): List of :class:`~plexapi.myplex.ResourceConnection` objects
|
||||||
for this resource.
|
for this resource.
|
||||||
createdAt (datetime): Timestamp this resource first connected to your server.
|
createdAt (datetime): Timestamp this resource first connected to your server.
|
||||||
device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc).
|
device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc).
|
||||||
@@ -930,7 +930,7 @@ class MyPlexResource(PlexObject):
|
|||||||
self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username.
|
self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username.
|
||||||
|
|
||||||
def connect(self, ssl=None, timeout=None):
|
def connect(self, ssl=None, timeout=None):
|
||||||
""" Returns a new :class:`~server.PlexServer` or :class:`~client.PlexClient` object.
|
""" Returns a new :class:`~plexapi.server.PlexServer` or :class:`~plexapi.client.PlexClient` object.
|
||||||
Often times there is more than one address specified for a server or client.
|
Often times there is more than one address specified for a server or client.
|
||||||
This function will prioritize local connections before remote and HTTPS before HTTP.
|
This function will prioritize local connections before remote and HTTPS before HTTP.
|
||||||
After trying to connect to all available addresses for this resource and
|
After trying to connect to all available addresses for this resource and
|
||||||
@@ -942,7 +942,7 @@ class MyPlexResource(PlexObject):
|
|||||||
HTTP or HTTPS connection.
|
HTTP or HTTPS connection.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
:exc:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
||||||
"""
|
"""
|
||||||
# Sort connections from (https, local) to (http, remote)
|
# Sort connections from (https, local) to (http, remote)
|
||||||
# Only check non-local connections unless we own the resource
|
# Only check non-local connections unless we own the resource
|
||||||
@@ -965,7 +965,7 @@ class MyPlexResource(PlexObject):
|
|||||||
|
|
||||||
class ResourceConnection(PlexObject):
|
class ResourceConnection(PlexObject):
|
||||||
""" Represents a Resource Connection object found within the
|
""" Represents a Resource Connection object found within the
|
||||||
:class:`~myplex.MyPlexResource` objects.
|
:class:`~plexapi.myplex.MyPlexResource` objects.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Connection'
|
TAG (str): 'Connection'
|
||||||
@@ -1049,7 +1049,7 @@ class MyPlexDevice(PlexObject):
|
|||||||
at least one connection was successful, the PlexClient object is built and returned.
|
at least one connection was successful, the PlexClient object is built and returned.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
|
:exc:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
|
||||||
"""
|
"""
|
||||||
cls = PlexServer if 'server' in self.provides else PlexClient
|
cls = PlexServer if 'server' in self.provides else PlexClient
|
||||||
listargs = [[cls, url, self.token, timeout] for url in self.connections]
|
listargs = [[cls, url, self.token, timeout] for url in self.connections]
|
||||||
@@ -1063,10 +1063,10 @@ class MyPlexDevice(PlexObject):
|
|||||||
self._server.query(key, self._server._session.delete)
|
self._server.query(key, self._server._session.delete)
|
||||||
|
|
||||||
def syncItems(self):
|
def syncItems(self):
|
||||||
""" Returns an instance of :class:`plexapi.sync.SyncList` for current device.
|
""" Returns an instance of :class:`~plexapi.sync.SyncList` for current device.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`.
|
:exc:`plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`.
|
||||||
"""
|
"""
|
||||||
if 'sync-target' not in self.provides:
|
if 'sync-target' not in self.provides:
|
||||||
raise BadRequest('Requested syncList for device which do not provides sync-target')
|
raise BadRequest('Requested syncList for device which do not provides sync-target')
|
||||||
@@ -1082,12 +1082,12 @@ class MyPlexPinLogin(object):
|
|||||||
This helper class supports a polling, threaded and callback approach.
|
This helper class supports a polling, threaded and callback approach.
|
||||||
|
|
||||||
- The polling approach expects the developer to periodically check if the PIN login was
|
- The polling approach expects the developer to periodically check if the PIN login was
|
||||||
successful using :func:`plexapi.myplex.MyPlexPinLogin.checkLogin`.
|
successful using :func:`~plexapi.myplex.MyPlexPinLogin.checkLogin`.
|
||||||
- The threaded approach expects the developer to call
|
- The threaded approach expects the developer to call
|
||||||
:func:`plexapi.myplex.MyPlexPinLogin.run` and then at a later time call
|
:func:`~plexapi.myplex.MyPlexPinLogin.run` and then at a later time call
|
||||||
:func:`plexapi.myplex.MyPlexPinLogin.waitForLogin` to wait for and check the result.
|
:func:`~plexapi.myplex.MyPlexPinLogin.waitForLogin` to wait for and check the result.
|
||||||
- The callback approach is an extension of the threaded approach and expects the developer
|
- The callback approach is an extension of the threaded approach and expects the developer
|
||||||
to pass the `callback` parameter to the call to :func:`plexapi.myplex.MyPlexPinLogin.run`.
|
to pass the `callback` parameter to the call to :func:`~plexapi.myplex.MyPlexPinLogin.run`.
|
||||||
The callback will be called when the thread waiting for the PIN login to succeed either
|
The callback will be called when the thread waiting for the PIN login to succeed either
|
||||||
finishes or expires. The parameter passed to the callback is the received authentication
|
finishes or expires. The parameter passed to the callback is the received authentication
|
||||||
token or `None` if the login expired.
|
token or `None` if the login expired.
|
||||||
|
|||||||
@@ -38,14 +38,18 @@ class Photoalbum(PlexPartialObject):
|
|||||||
self.composite = data.attrib.get('composite')
|
self.composite = data.attrib.get('composite')
|
||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||||
|
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.ratingKey = data.attrib.get('ratingKey')
|
self.ratingKey = data.attrib.get('ratingKey')
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
|
self.titleSort = data.attrib.get('titleSort')
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
|
||||||
def albums(self, **kwargs):
|
def albums(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.photo.Photoalbum` objects in this album. """
|
""" Returns a list of :class:`~plexapi.photo.Photoalbum` objects in this album. """
|
||||||
@@ -71,6 +75,11 @@ class Photoalbum(PlexPartialObject):
|
|||||||
return photo
|
return photo
|
||||||
raise NotFound('Unable to find photo: %s' % title)
|
raise NotFound('Unable to find photo: %s' % title)
|
||||||
|
|
||||||
|
def clips(self, **kwargs):
|
||||||
|
""" Returns a list of :class:`~plexapi.video.Clip` objects in this album. """
|
||||||
|
key = '/library/metadata/%s/children' % self.ratingKey
|
||||||
|
return self.fetchItems(key, etag='Video', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Photo(PlexPartialObject):
|
class Photo(PlexPartialObject):
|
||||||
@@ -99,25 +108,43 @@ class Photo(PlexPartialObject):
|
|||||||
TYPE = 'photo'
|
TYPE = 'photo'
|
||||||
METADATA_TYPE = 'photo'
|
METADATA_TYPE = 'photo'
|
||||||
|
|
||||||
|
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
|
||||||
|
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
|
||||||
|
'&includeMarkers=1&includeConcerts=1&includePreferences=1'
|
||||||
|
'&includeBandwidths=1&includeLoudnessRamps=1')
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
|
self.key = data.attrib.get('key')
|
||||||
|
self._details_key = self.key + self._include
|
||||||
self.listType = 'photo'
|
self.listType = 'photo'
|
||||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||||
|
self.createdAtAccuracy = data.attrib.get('createdAtAccuracy')
|
||||||
|
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
|
||||||
|
self.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key')
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||||
|
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.originallyAvailableAt = utils.toDatetime(
|
self.originallyAvailableAt = utils.toDatetime(
|
||||||
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
|
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = data.attrib.get('parentRatingKey')
|
self.parentRatingKey = data.attrib.get('parentRatingKey')
|
||||||
|
self.parentThumb = data.attrib.get('parentThumb')
|
||||||
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
self.ratingKey = data.attrib.get('ratingKey')
|
self.ratingKey = data.attrib.get('ratingKey')
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
|
self.titleSort = data.attrib.get('titleSort')
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
self.media = self.findItems(data, media.Media)
|
self.media = self.findItems(data, media.Media)
|
||||||
self.tag = self.findItems(data, media.Tag)
|
self.tag = self.findItems(data, media.Tag)
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
|
||||||
def photoalbum(self):
|
def photoalbum(self):
|
||||||
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
|
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
|
||||||
@@ -132,22 +159,29 @@ class Photo(PlexPartialObject):
|
|||||||
else:
|
else:
|
||||||
raise BadRequest('Unable to get section for photo, can`t find librarySectionID')
|
raise BadRequest('Unable to get section for photo, can`t find librarySectionID')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locations(self):
|
||||||
|
""" This does not exist in plex xml response but is added to have a common
|
||||||
|
interface to get the location of the Photo
|
||||||
|
"""
|
||||||
|
return [part.file for item in self.media for part in item.parts if part]
|
||||||
|
|
||||||
def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
|
def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
|
||||||
""" Add current photo as sync item for specified device.
|
""" Add current photo as sync item for specified device.
|
||||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
|
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
|
||||||
module :mod:`plexapi.sync`.
|
module :mod:`~plexapi.sync`.
|
||||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
:func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
limit (int): maximum count of items to sync, unlimited if `None`.
|
limit (int): maximum count of items to sync, unlimited if `None`.
|
||||||
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
|
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
|
||||||
generated from metadata of current photo.
|
generated from metadata of current photo.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plexapi.sync import SyncItem, Policy, MediaSettings
|
from plexapi.sync import SyncItem, Policy, MediaSettings
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class Playlist(PlexPartialObject, Playable):
|
|||||||
**kwargs (dict): is passed to the filters. For a example see the search method.
|
**kwargs (dict): is passed to the filters. For a example see the search method.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.playlist.Playlist`: an instance of created Playlist.
|
:class:`~plexapi.playlist.Playlist`: an instance of created Playlist.
|
||||||
"""
|
"""
|
||||||
if smart:
|
if smart:
|
||||||
return cls._createSmart(server, title, section, limit, **kwargs)
|
return cls._createSmart(server, title, section, limit, **kwargs)
|
||||||
@@ -217,29 +217,29 @@ class Playlist(PlexPartialObject, Playable):
|
|||||||
def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None,
|
def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, client=None, clientId=None, limit=None,
|
||||||
unwatched=False, title=None):
|
unwatched=False, title=None):
|
||||||
""" Add current playlist as sync item for specified device.
|
""" Add current playlist as sync item for specified device.
|
||||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
||||||
:mod:`plexapi.sync` module. Used only when playlist contains video.
|
:mod:`~plexapi.sync` module. Used only when playlist contains video.
|
||||||
photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in
|
photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in
|
||||||
the module :mod:`plexapi.sync`. Used only when playlist contains photos.
|
the module :mod:`~plexapi.sync`. Used only when playlist contains photos.
|
||||||
audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values
|
audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values
|
||||||
from the module :mod:`plexapi.sync`. Used only when playlist contains audio.
|
from the module :mod:`~plexapi.sync`. Used only when playlist contains audio.
|
||||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
:func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
limit (int): maximum count of items to sync, unlimited if `None`.
|
limit (int): maximum count of items to sync, unlimited if `None`.
|
||||||
unwatched (bool): if `True` watched videos wouldn't be synced.
|
unwatched (bool): if `True` watched videos wouldn't be synced.
|
||||||
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
|
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
|
||||||
generated from metadata of current photo.
|
generated from metadata of current photo.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync.
|
:exc:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync.
|
||||||
:class:`plexapi.exceptions.Unsupported`: when playlist content is unsupported.
|
:exc:`plexapi.exceptions.Unsupported`: when playlist content is unsupported.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.allowSync:
|
if not self.allowSync:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from plexapi.base import PlexObject
|
|||||||
from plexapi.client import PlexClient
|
from plexapi.client import PlexClient
|
||||||
from plexapi.compat import ElementTree, urlencode
|
from plexapi.compat import ElementTree, urlencode
|
||||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||||
from plexapi.library import Library, Hub
|
from plexapi.library import Hub, Library, Path, File
|
||||||
from plexapi.settings import Settings
|
from plexapi.settings import Settings
|
||||||
from plexapi.playlist import Playlist
|
from plexapi.playlist import Playlist
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
@@ -185,7 +185,7 @@ class PlexServer(PlexObject):
|
|||||||
return Account(self, data)
|
return Account(self, data)
|
||||||
|
|
||||||
def agents(self, mediaType=None):
|
def agents(self, mediaType=None):
|
||||||
""" Returns the `:class:`~plexapi.media.Agent` objects this server has available. """
|
""" Returns the :class:`~plexapi.media.Agent` objects this server has available. """
|
||||||
key = '/system/agents'
|
key = '/system/agents'
|
||||||
if mediaType:
|
if mediaType:
|
||||||
key += '?mediaType=%s' % mediaType
|
key += '?mediaType=%s' % mediaType
|
||||||
@@ -233,6 +233,53 @@ class PlexServer(PlexObject):
|
|||||||
log.warning('Unable to fetch client ports from myPlex: %s', err)
|
log.warning('Unable to fetch client ports from myPlex: %s', err)
|
||||||
return ports
|
return ports
|
||||||
|
|
||||||
|
def browse(self, path=None, includeFiles=True):
|
||||||
|
""" Browse the system file path using the Plex API.
|
||||||
|
Returns list of :class:`~plexapi.library.Path` and :class:`~plexapi.library.File` objects.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
path (:class:`~plexapi.library.Path` or str, optional): Full path to browse.
|
||||||
|
includeFiles (bool): True to include files when browsing (Default).
|
||||||
|
False to only return folders.
|
||||||
|
"""
|
||||||
|
if isinstance(path, Path):
|
||||||
|
key = path.key
|
||||||
|
elif path is not None:
|
||||||
|
base64path = utils.base64str(path)
|
||||||
|
key = '/services/browse/%s' % base64path
|
||||||
|
else:
|
||||||
|
key = '/services/browse'
|
||||||
|
if includeFiles:
|
||||||
|
key += '?includeFiles=1'
|
||||||
|
return self.fetchItems(key)
|
||||||
|
|
||||||
|
def walk(self, path=None):
|
||||||
|
""" Walk the system file tree using the Plex API similar to `os.walk`.
|
||||||
|
Yields a 3-tuple `(path, paths, files)` where
|
||||||
|
`path` is a string of the directory path,
|
||||||
|
`paths` is a list of :class:`~plexapi.library.Path` objects, and
|
||||||
|
`files` is a list of :class:`~plexapi.library.File` objects.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
path (:class:`~plexapi.library.Path` or str, optional): Full path to walk.
|
||||||
|
"""
|
||||||
|
paths = []
|
||||||
|
files = []
|
||||||
|
for item in self.browse(path):
|
||||||
|
if isinstance(item, Path):
|
||||||
|
paths.append(item)
|
||||||
|
elif isinstance(item, File):
|
||||||
|
files.append(item)
|
||||||
|
|
||||||
|
if isinstance(path, Path):
|
||||||
|
path = path.path
|
||||||
|
|
||||||
|
yield path or '', paths, files
|
||||||
|
|
||||||
|
for _path in paths:
|
||||||
|
for path, paths, files in self.walk(_path):
|
||||||
|
yield path, paths, files
|
||||||
|
|
||||||
def clients(self):
|
def clients(self):
|
||||||
""" Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """
|
""" Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """
|
||||||
items = []
|
items = []
|
||||||
@@ -256,7 +303,7 @@ class PlexServer(PlexObject):
|
|||||||
name (str): Name of the client to return.
|
name (str): Name of the client to return.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.NotFound`: Unknown client name
|
:exc:`plexapi.exceptions.NotFound`: Unknown client name
|
||||||
"""
|
"""
|
||||||
for client in self.clients():
|
for client in self.clients():
|
||||||
if client and client.title == name:
|
if client and client.title == name:
|
||||||
@@ -366,11 +413,11 @@ class PlexServer(PlexObject):
|
|||||||
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
|
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def playlists(self):
|
def playlists(self, **kwargs):
|
||||||
""" Returns a list of all :class:`~plexapi.playlist.Playlist` objects saved on the server. """
|
""" Returns a list of all :class:`~plexapi.playlist.Playlist` objects saved on the server. """
|
||||||
# TODO: Add sort and type options?
|
# TODO: Add sort and type options?
|
||||||
# /playlists/all?type=15&sort=titleSort%3Aasc&playlistType=video&smart=0
|
# /playlists/all?type=15&sort=titleSort%3Aasc&playlistType=video&smart=0
|
||||||
return self.fetchItems('/playlists')
|
return self.fetchItems('/playlists', **kwargs)
|
||||||
|
|
||||||
def playlist(self, title):
|
def playlist(self, title):
|
||||||
""" Returns the :class:`~plexapi.client.Playlist` that matches the specified title.
|
""" Returns the :class:`~plexapi.client.Playlist` that matches the specified title.
|
||||||
@@ -379,7 +426,7 @@ class PlexServer(PlexObject):
|
|||||||
title (str): Title of the playlist to return.
|
title (str): Title of the playlist to return.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.NotFound`: Invalid playlist title
|
:exc:`plexapi.exceptions.NotFound`: Invalid playlist title
|
||||||
"""
|
"""
|
||||||
return self.fetchItem('/playlists', title=title)
|
return self.fetchItem('/playlists', title=title)
|
||||||
|
|
||||||
@@ -480,8 +527,8 @@ class PlexServer(PlexObject):
|
|||||||
Parameters:
|
Parameters:
|
||||||
callback (func): Callback function to call on recieved messages.
|
callback (func): Callback function to call on recieved messages.
|
||||||
|
|
||||||
raises:
|
Raises:
|
||||||
:class:`plexapi.exception.Unsupported`: Websocket-client not installed.
|
:exc:`plexapi.exception.Unsupported`: Websocket-client not installed.
|
||||||
"""
|
"""
|
||||||
notifier = AlertListener(self, callback)
|
notifier = AlertListener(self, callback)
|
||||||
notifier.start()
|
notifier.start()
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ class Settings(PlexObject):
|
|||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
if attr.startswith('_'):
|
if attr.startswith('_'):
|
||||||
|
try:
|
||||||
return self.__dict__[attr]
|
return self.__dict__[attr]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError
|
||||||
return self.get(attr).value
|
return self.get(attr).value
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class SyncItem(PlexObject):
|
|||||||
self.location = data.find('Location').attrib.get('uri', '')
|
self.location = data.find('Location').attrib.get('uri', '')
|
||||||
|
|
||||||
def server(self):
|
def server(self):
|
||||||
""" Returns :class:`plexapi.myplex.MyPlexResource` with server of current item. """
|
""" Returns :class:`~plexapi.myplex.MyPlexResource` with server of current item. """
|
||||||
server = [s for s in self._server.resources() if s.clientIdentifier == self.machineIdentifier]
|
server = [s for s in self._server.resources() if s.clientIdentifier == self.machineIdentifier]
|
||||||
if len(server) == 0:
|
if len(server) == 0:
|
||||||
raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier)
|
raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier)
|
||||||
@@ -201,7 +201,7 @@ class MediaSettings(object):
|
|||||||
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in this module.
|
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in this module.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: when provided unknown video quality.
|
:exc:`plexapi.exceptions.BadRequest`: when provided unknown video quality.
|
||||||
"""
|
"""
|
||||||
if videoQuality == VIDEO_QUALITY_ORIGINAL:
|
if videoQuality == VIDEO_QUALITY_ORIGINAL:
|
||||||
return MediaSettings('', '', '')
|
return MediaSettings('', '', '')
|
||||||
@@ -231,7 +231,7 @@ class MediaSettings(object):
|
|||||||
module.
|
module.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest` when provided unknown video quality.
|
:exc:`plexapi.exceptions.BadRequest` when provided unknown video quality.
|
||||||
"""
|
"""
|
||||||
if resolution in PHOTO_QUALITIES:
|
if resolution in PHOTO_QUALITIES:
|
||||||
return MediaSettings(photoQuality=PHOTO_QUALITIES[resolution], photoResolution=resolution)
|
return MediaSettings(photoQuality=PHOTO_QUALITIES[resolution], photoResolution=resolution)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import base64
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -23,7 +24,8 @@ log = logging.getLogger('plexapi')
|
|||||||
# Library Types - Populated at runtime
|
# Library Types - Populated at runtime
|
||||||
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, 'trailer': 5, 'comic': 6, 'person': 7,
|
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, 'trailer': 5, 'comic': 6, 'person': 7,
|
||||||
'artist': 8, 'album': 9, 'track': 10, 'picture': 11, 'clip': 12, 'photo': 13, 'photoalbum': 14,
|
'artist': 8, 'album': 9, 'track': 10, 'picture': 11, 'clip': 12, 'photo': 13, 'photoalbum': 14,
|
||||||
'playlist': 15, 'playlistFolder': 16, 'collection': 18, 'userPlaylistItem': 1001}
|
'playlist': 15, 'playlistFolder': 16, 'collection': 18,
|
||||||
|
'optimizedVersion': 42, 'userPlaylistItem': 1001}
|
||||||
PLEXOBJECTS = {}
|
PLEXOBJECTS = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -146,7 +148,7 @@ def searchType(libtype):
|
|||||||
libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track,
|
libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track,
|
||||||
collection)
|
collection)
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.NotFound`: Unknown libtype
|
:exc:`plexapi.exceptions.NotFound`: Unknown libtype
|
||||||
"""
|
"""
|
||||||
libtype = compat.ustr(libtype)
|
libtype = compat.ustr(libtype)
|
||||||
if libtype in [compat.ustr(v) for v in SEARCHTYPES.values()]:
|
if libtype in [compat.ustr(v) for v in SEARCHTYPES.values()]:
|
||||||
@@ -398,3 +400,7 @@ def getAgentIdentifier(section, agent):
|
|||||||
agents += identifiers
|
agents += identifiers
|
||||||
raise NotFound('Couldnt find "%s" in agents list (%s)' %
|
raise NotFound('Couldnt find "%s" in agents list (%s)' %
|
||||||
(agent, ', '.join(agents)))
|
(agent, ', '.join(agents)))
|
||||||
|
|
||||||
|
|
||||||
|
def base64str(text):
|
||||||
|
return base64.b64encode(text.encode('utf-8')).decode('utf-8')
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class Video(PlexPartialObject):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
addedAt (datetime): Datetime this item was added to the library.
|
addedAt (datetime): Datetime this item was added to the library.
|
||||||
|
art (str): URL to artwork image.
|
||||||
|
artBlurHash (str): BlurHash string for artwork image.
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
lastViewedAt (datetime): Datetime item was last accessed.
|
lastViewedAt (datetime): Datetime item was last accessed.
|
||||||
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||||
@@ -20,6 +22,7 @@ class Video(PlexPartialObject):
|
|||||||
ratingKey (int): Unique key identifying this item.
|
ratingKey (int): Unique key identifying this item.
|
||||||
summary (str): Summary of the artist, track, or album.
|
summary (str): Summary of the artist, track, or album.
|
||||||
thumb (str): URL to thumbnail image.
|
thumb (str): URL to thumbnail image.
|
||||||
|
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||||
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'artist', 'album', or 'track'.
|
type (str): 'artist', 'album', or 'track'.
|
||||||
@@ -32,12 +35,17 @@ class Video(PlexPartialObject):
|
|||||||
self._data = data
|
self._data = data
|
||||||
self.listType = 'video'
|
self.listType = 'video'
|
||||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||||
|
self.art = data.attrib.get('art')
|
||||||
|
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||||
self.key = data.attrib.get('key', '')
|
self.key = data.attrib.get('key', '')
|
||||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||||
|
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
@@ -199,21 +207,21 @@ class Video(PlexPartialObject):
|
|||||||
|
|
||||||
def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None):
|
def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None):
|
||||||
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
|
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
|
||||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
||||||
:mod:`plexapi.sync` module.
|
:mod:`~plexapi.sync` module.
|
||||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
:func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
|
||||||
limit (int): maximum count of items to sync, unlimited if `None`.
|
limit (int): maximum count of items to sync, unlimited if `None`.
|
||||||
unwatched (bool): if `True` watched videos wouldn't be synced.
|
unwatched (bool): if `True` watched videos wouldn't be synced.
|
||||||
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
|
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
|
||||||
generated from metadata of current media.
|
generated from metadata of current media.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plexapi.sync import SyncItem, Policy, MediaSettings
|
from plexapi.sync import SyncItem, Policy, MediaSettings
|
||||||
@@ -264,6 +272,7 @@ class Movie(Playable, Video):
|
|||||||
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
||||||
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
||||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||||
|
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||||
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
|
||||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||||
@@ -274,16 +283,12 @@ class Movie(Playable, Video):
|
|||||||
TAG = 'Video'
|
TAG = 'Video'
|
||||||
TYPE = 'movie'
|
TYPE = 'movie'
|
||||||
METADATA_TYPE = 'movie'
|
METADATA_TYPE = 'movie'
|
||||||
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
|
|
||||||
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
|
|
||||||
'&includeConcerts=1&includePreferences=1')
|
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
Video._loadData(self, data)
|
Video._loadData(self, data)
|
||||||
Playable._loadData(self, data)
|
Playable._loadData(self, data)
|
||||||
|
|
||||||
self._details_key = self.key + self._include
|
|
||||||
self.art = data.attrib.get('art')
|
self.art = data.attrib.get('art')
|
||||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||||
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
||||||
@@ -307,6 +312,7 @@ class Movie(Playable, Video):
|
|||||||
self.directors = self.findItems(data, media.Director)
|
self.directors = self.findItems(data, media.Director)
|
||||||
self.fields = self.findItems(data, media.Field)
|
self.fields = self.findItems(data, media.Field)
|
||||||
self.genres = self.findItems(data, media.Genre)
|
self.genres = self.findItems(data, media.Genre)
|
||||||
|
self.guids = self.findItems(data, media.Guid)
|
||||||
self.media = self.findItems(data, media.Media)
|
self.media = self.findItems(data, media.Media)
|
||||||
self.producers = self.findItems(data, media.Producer)
|
self.producers = self.findItems(data, media.Producer)
|
||||||
self.roles = self.findItems(data, media.Role)
|
self.roles = self.findItems(data, media.Role)
|
||||||
@@ -323,7 +329,7 @@ class Movie(Playable, Video):
|
|||||||
@property
|
@property
|
||||||
def locations(self):
|
def locations(self):
|
||||||
""" This does not exist in plex xml response but is added to have a common
|
""" This does not exist in plex xml response but is added to have a common
|
||||||
interface to get the location of the Movie/Show/Episode
|
interface to get the location of the Movie
|
||||||
"""
|
"""
|
||||||
return [part.file for part in self.iterParts() if part]
|
return [part.file for part in self.iterParts() if part]
|
||||||
|
|
||||||
@@ -338,7 +344,7 @@ class Movie(Playable, Video):
|
|||||||
savepath (str): Defaults to current working dir.
|
savepath (str): Defaults to current working dir.
|
||||||
keep_original_name (bool): True to keep the original file name otherwise
|
keep_original_name (bool): True to keep the original file name otherwise
|
||||||
a friendlier is generated.
|
a friendlier is generated.
|
||||||
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
|
||||||
"""
|
"""
|
||||||
filepaths = []
|
filepaths = []
|
||||||
locations = [i for i in self.iterParts() if i]
|
locations = [i for i in self.iterParts() if i]
|
||||||
@@ -397,7 +403,7 @@ class Show(Video):
|
|||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
Video._loadData(self, data)
|
Video._loadData(self, data)
|
||||||
# fix key if loaded from search
|
# fix key if loaded from search
|
||||||
self.key = self.key.replace('/children', '')
|
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||||
self.art = data.attrib.get('art')
|
self.art = data.attrib.get('art')
|
||||||
self.banner = data.attrib.get('banner')
|
self.banner = data.attrib.get('banner')
|
||||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||||
@@ -415,6 +421,7 @@ class Show(Video):
|
|||||||
self.theme = data.attrib.get('theme')
|
self.theme = data.attrib.get('theme')
|
||||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
self.genres = self.findItems(data, media.Genre)
|
self.genres = self.findItems(data, media.Genre)
|
||||||
self.roles = self.findItems(data, media.Role)
|
self.roles = self.findItems(data, media.Role)
|
||||||
self.labels = self.findItems(data, media.Label)
|
self.labels = self.findItems(data, media.Label)
|
||||||
@@ -460,8 +467,8 @@ class Show(Video):
|
|||||||
episode (int): Episode number (default:None; required if title not specified).
|
episode (int): Episode number (default:None; required if title not specified).
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`plexapi.exceptions.BadRequest`: If season and episode is missing.
|
:exc:`plexapi.exceptions.BadRequest`: If season and episode is missing.
|
||||||
:class:`plexapi.exceptions.NotFound`: If the episode is missing.
|
:exc:`plexapi.exceptions.NotFound`: If the episode is missing.
|
||||||
"""
|
"""
|
||||||
if title:
|
if title:
|
||||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||||
@@ -482,7 +489,7 @@ class Show(Video):
|
|||||||
return self.episodes(viewCount=0)
|
return self.episodes(viewCount=0)
|
||||||
|
|
||||||
def get(self, title=None, season=None, episode=None):
|
def get(self, title=None, season=None, episode=None):
|
||||||
""" Alias to :func:`~plexapi.video.Show.episode()`. """
|
""" Alias to :func:`~plexapi.video.Show.episode`. """
|
||||||
return self.episode(title, season, episode)
|
return self.episode(title, season, episode)
|
||||||
|
|
||||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||||
@@ -492,7 +499,7 @@ class Show(Video):
|
|||||||
savepath (str): Defaults to current working dir.
|
savepath (str): Defaults to current working dir.
|
||||||
keep_original_name (bool): True to keep the original file name otherwise
|
keep_original_name (bool): True to keep the original file name otherwise
|
||||||
a friendlier is generated.
|
a friendlier is generated.
|
||||||
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
|
||||||
"""
|
"""
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for episode in self.episodes():
|
for episode in self.episodes():
|
||||||
@@ -527,12 +534,20 @@ class Season(Video):
|
|||||||
Video._loadData(self, data)
|
Video._loadData(self, data)
|
||||||
# fix key if loaded from search
|
# fix key if loaded from search
|
||||||
self.key = self.key.replace('/children', '')
|
self.key = self.key.replace('/children', '')
|
||||||
|
art = data.attrib.get('art')
|
||||||
|
self.art = art if art and str(self.ratingKey) in art else None
|
||||||
|
self.guid = data.attrib.get('guid')
|
||||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
|
self.parentIndex = data.attrib.get('parentIndex')
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||||
|
self.parentTheme = data.attrib.get('parentTheme')
|
||||||
|
self.parentThumb = data.attrib.get('parentThumb')
|
||||||
self.parentTitle = data.attrib.get('parentTitle')
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s>' % ':'.join([p for p in [
|
return '<%s>' % ':'.join([p for p in [
|
||||||
@@ -571,7 +586,7 @@ class Season(Video):
|
|||||||
return self.fetchItem(key, parentIndex=self.index, index=episode)
|
return self.fetchItem(key, parentIndex=self.index, index=episode)
|
||||||
|
|
||||||
def get(self, title=None, episode=None):
|
def get(self, title=None, episode=None):
|
||||||
""" Alias to :func:`~plexapi.video.Season.episode()`. """
|
""" Alias to :func:`~plexapi.video.Season.episode`. """
|
||||||
return self.episode(title, episode)
|
return self.episode(title, episode)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
@@ -593,7 +608,7 @@ class Season(Video):
|
|||||||
savepath (str): Defaults to current working dir.
|
savepath (str): Defaults to current working dir.
|
||||||
keep_original_name (bool): True to keep the original file name otherwise
|
keep_original_name (bool): True to keep the original file name otherwise
|
||||||
a friendlier is generated.
|
a friendlier is generated.
|
||||||
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`.
|
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL`.
|
||||||
"""
|
"""
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for episode in self.episodes():
|
for episode in self.episodes():
|
||||||
@@ -642,21 +657,18 @@ class Episode(Playable, Video):
|
|||||||
TYPE = 'episode'
|
TYPE = 'episode'
|
||||||
METADATA_TYPE = 'episode'
|
METADATA_TYPE = 'episode'
|
||||||
|
|
||||||
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
|
|
||||||
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
|
|
||||||
'&includeConcerts=1&includePreferences=1')
|
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
Video._loadData(self, data)
|
Video._loadData(self, data)
|
||||||
Playable._loadData(self, data)
|
Playable._loadData(self, data)
|
||||||
self._details_key = self.key + self._include
|
|
||||||
self._seasonNumber = None # cached season number
|
self._seasonNumber = None # cached season number
|
||||||
self.art = data.attrib.get('art')
|
art = data.attrib.get('art')
|
||||||
|
self.art = art if art and str(self.ratingKey) in art else None
|
||||||
self.chapterSource = data.attrib.get('chapterSource')
|
self.chapterSource = data.attrib.get('chapterSource')
|
||||||
self.contentRating = data.attrib.get('contentRating')
|
self.contentRating = data.attrib.get('contentRating')
|
||||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||||
self.grandparentArt = data.attrib.get('grandparentArt')
|
self.grandparentArt = data.attrib.get('grandparentArt')
|
||||||
|
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
||||||
self.grandparentKey = data.attrib.get('grandparentKey')
|
self.grandparentKey = data.attrib.get('grandparentKey')
|
||||||
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
||||||
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
||||||
@@ -665,6 +677,7 @@ class Episode(Playable, Video):
|
|||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
self.parentIndex = data.attrib.get('parentIndex')
|
self.parentIndex = data.attrib.get('parentIndex')
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||||
@@ -675,11 +688,13 @@ class Episode(Playable, Video):
|
|||||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
self.directors = self.findItems(data, media.Director)
|
self.directors = self.findItems(data, media.Director)
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
self.media = self.findItems(data, media.Media)
|
self.media = self.findItems(data, media.Media)
|
||||||
self.writers = self.findItems(data, media.Writer)
|
self.writers = self.findItems(data, media.Writer)
|
||||||
self.labels = self.findItems(data, media.Label)
|
self.labels = self.findItems(data, media.Label)
|
||||||
self.collections = self.findItems(data, media.Collection)
|
self.collections = self.findItems(data, media.Collection)
|
||||||
self.chapters = self.findItems(data, media.Chapter)
|
self.chapters = self.findItems(data, media.Chapter)
|
||||||
|
self.markers = self.findItems(data, media.Marker)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s>' % ':'.join([p for p in [
|
return '<%s>' % ':'.join([p for p in [
|
||||||
@@ -695,7 +710,7 @@ class Episode(Playable, Video):
|
|||||||
@property
|
@property
|
||||||
def locations(self):
|
def locations(self):
|
||||||
""" This does not exist in plex xml response but is added to have a common
|
""" This does not exist in plex xml response but is added to have a common
|
||||||
interface to get the location of the Movie/Show
|
interface to get the location of the Episode
|
||||||
"""
|
"""
|
||||||
return [part.file for part in self.iterParts() if part]
|
return [part.file for part in self.iterParts() if part]
|
||||||
|
|
||||||
@@ -711,6 +726,13 @@ class Episode(Playable, Video):
|
|||||||
""" Returns the s00e00 string containing the season and episode. """
|
""" Returns the s00e00 string containing the season and episode. """
|
||||||
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.index).zfill(2))
|
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.index).zfill(2))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hasIntroMarker(self):
|
||||||
|
""" Returns True if this episode has an intro marker in the xml. """
|
||||||
|
if not self.isFullObject():
|
||||||
|
self.reload()
|
||||||
|
return any(marker.type == 'intro' for marker in self.markers)
|
||||||
|
|
||||||
def season(self):
|
def season(self):
|
||||||
"""" Return this episodes :func:`~plexapi.video.Season`.. """
|
"""" Return this episodes :func:`~plexapi.video.Season`.. """
|
||||||
return self.fetchItem(self.parentKey)
|
return self.fetchItem(self.parentKey)
|
||||||
@@ -733,12 +755,15 @@ class Clip(Playable, Video):
|
|||||||
METADATA_TYPE = 'clip'
|
METADATA_TYPE = 'clip'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
|
""" Load attribute values from Plex XML response. """
|
||||||
|
Video._loadData(self, data)
|
||||||
|
Playable._loadData(self, data)
|
||||||
self._data = data
|
self._data = data
|
||||||
self.addedAt = data.attrib.get('addedAt')
|
self.addedAt = data.attrib.get('addedAt')
|
||||||
self.duration = data.attrib.get('duration')
|
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
self.originallyAvailableAt = data.attrib.get('originallyAvailableAt')
|
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
self.ratingKey = data.attrib.get('ratingKey')
|
self.ratingKey = data.attrib.get('ratingKey')
|
||||||
self.skipDetails = utils.cast(int, data.attrib.get('skipDetails'))
|
self.skipDetails = utils.cast(int, data.attrib.get('skipDetails'))
|
||||||
self.subtype = data.attrib.get('subtype')
|
self.subtype = data.attrib.get('subtype')
|
||||||
@@ -746,4 +771,12 @@ class Clip(Playable, Video):
|
|||||||
self.thumbAspectRatio = data.attrib.get('thumbAspectRatio')
|
self.thumbAspectRatio = data.attrib.get('thumbAspectRatio')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
self.year = data.attrib.get('year')
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
|
self.media = self.findItems(data, media.Media)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locations(self):
|
||||||
|
""" This does not exist in plex xml response but is added to have a common
|
||||||
|
interface to get the location of the Clip
|
||||||
|
"""
|
||||||
|
return [part.file for part in self.iterParts() if part]
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
from ._tqdm import tqdm
|
|
||||||
from ._tqdm import trange
|
|
||||||
from ._tqdm_gui import tqdm_gui
|
|
||||||
from ._tqdm_gui import tgrange
|
|
||||||
from ._tqdm_pandas import tqdm_pandas
|
|
||||||
from ._main import main
|
|
||||||
from ._monitor import TMonitor, TqdmSynchronisationWarning
|
|
||||||
from ._version import __version__ # NOQA
|
|
||||||
from ._tqdm import TqdmTypeError, TqdmKeyError, TqdmWarning, \
|
|
||||||
TqdmDeprecationWarning, TqdmExperimentalWarning, \
|
|
||||||
TqdmMonitorWarning
|
|
||||||
|
|
||||||
__all__ = ['tqdm', 'tqdm_gui', 'trange', 'tgrange', 'tqdm_pandas',
|
|
||||||
'tqdm_notebook', 'tnrange', 'main', 'TMonitor',
|
|
||||||
'TqdmTypeError', 'TqdmKeyError',
|
|
||||||
'TqdmWarning', 'TqdmDeprecationWarning',
|
|
||||||
'TqdmExperimentalWarning',
|
|
||||||
'TqdmMonitorWarning', 'TqdmSynchronisationWarning',
|
|
||||||
'__version__']
|
|
||||||
|
|
||||||
|
|
||||||
def tqdm_notebook(*args, **kwargs): # pragma: no cover
|
|
||||||
"""See tqdm._tqdm_notebook.tqdm_notebook for full documentation"""
|
|
||||||
from ._tqdm_notebook import tqdm_notebook as _tqdm_notebook
|
|
||||||
return _tqdm_notebook(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def tnrange(*args, **kwargs): # pragma: no cover
|
|
||||||
"""
|
|
||||||
A shortcut for tqdm_notebook(xrange(*args), **kwargs).
|
|
||||||
On Python3+ range is used instead of xrange.
|
|
||||||
"""
|
|
||||||
from ._tqdm_notebook import tnrange as _tnrange
|
|
||||||
return _tnrange(*args, **kwargs)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from ._main import main
|
|
||||||
main()
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
from ._tqdm import tqdm, TqdmTypeError, TqdmKeyError
|
|
||||||
from ._version import __version__ # NOQA
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
__all__ = ["main"]
|
|
||||||
|
|
||||||
|
|
||||||
def cast(val, typ):
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
log.debug((val, typ))
|
|
||||||
if " or " in typ:
|
|
||||||
for t in typ.split(" or "):
|
|
||||||
try:
|
|
||||||
return cast(val, t)
|
|
||||||
except TqdmTypeError:
|
|
||||||
pass
|
|
||||||
raise TqdmTypeError(val + ' : ' + typ)
|
|
||||||
|
|
||||||
# sys.stderr.write('\ndebug | `val:type`: `' + val + ':' + typ + '`.\n')
|
|
||||||
if typ == 'bool':
|
|
||||||
if (val == 'True') or (val == ''):
|
|
||||||
return True
|
|
||||||
elif val == 'False':
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
raise TqdmTypeError(val + ' : ' + typ)
|
|
||||||
try:
|
|
||||||
return eval(typ + '("' + val + '")')
|
|
||||||
except:
|
|
||||||
if typ == 'chr':
|
|
||||||
return chr(ord(eval('"' + val + '"')))
|
|
||||||
else:
|
|
||||||
raise TqdmTypeError(val + ' : ' + typ)
|
|
||||||
|
|
||||||
|
|
||||||
def posix_pipe(fin, fout, delim='\n', buf_size=256,
|
|
||||||
callback=lambda int: None # pragma: no cover
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Params
|
|
||||||
------
|
|
||||||
fin : file with `read(buf_size : int)` method
|
|
||||||
fout : file with `write` (and optionally `flush`) methods.
|
|
||||||
callback : function(int), e.g.: `tqdm.update`
|
|
||||||
"""
|
|
||||||
fp_write = fout.write
|
|
||||||
|
|
||||||
# tmp = ''
|
|
||||||
if not delim:
|
|
||||||
while True:
|
|
||||||
tmp = fin.read(buf_size)
|
|
||||||
|
|
||||||
# flush at EOF
|
|
||||||
if not tmp:
|
|
||||||
getattr(fout, 'flush', lambda: None)() # pragma: no cover
|
|
||||||
return
|
|
||||||
|
|
||||||
fp_write(tmp)
|
|
||||||
callback(len(tmp))
|
|
||||||
# return
|
|
||||||
|
|
||||||
buf = ''
|
|
||||||
# n = 0
|
|
||||||
while True:
|
|
||||||
tmp = fin.read(buf_size)
|
|
||||||
|
|
||||||
# flush at EOF
|
|
||||||
if not tmp:
|
|
||||||
if buf:
|
|
||||||
fp_write(buf)
|
|
||||||
callback(1 + buf.count(delim)) # n += 1 + buf.count(delim)
|
|
||||||
getattr(fout, 'flush', lambda: None)() # pragma: no cover
|
|
||||||
return # n
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
i = tmp.index(delim)
|
|
||||||
except ValueError:
|
|
||||||
buf += tmp
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
fp_write(buf + tmp[:i + len(delim)])
|
|
||||||
callback(1) # n += 1
|
|
||||||
buf = ''
|
|
||||||
tmp = tmp[i + len(delim):]
|
|
||||||
|
|
||||||
|
|
||||||
# ((opt, type), ... )
|
|
||||||
RE_OPTS = re.compile(r'\n {8}(\S+)\s{2,}:\s*([^,]+)')
|
|
||||||
# better split method assuming no positional args
|
|
||||||
RE_SHLEX = re.compile(r'\s*(?<!\S)--?([^\s=]+)(?:\s*|=|$)')
|
|
||||||
|
|
||||||
# TODO: add custom support for some of the following?
|
|
||||||
UNSUPPORTED_OPTS = ('iterable', 'gui', 'out', 'file')
|
|
||||||
|
|
||||||
# The 8 leading spaces are required for consistency
|
|
||||||
CLI_EXTRA_DOC = r"""
|
|
||||||
Extra CLI Options
|
|
||||||
-----------------
|
|
||||||
name : type, optional
|
|
||||||
TODO: find out why this is needed.
|
|
||||||
delim : chr, optional
|
|
||||||
Delimiting character [default: '\n']. Use '\0' for null.
|
|
||||||
N.B.: on Windows systems, Python converts '\n' to '\r\n'.
|
|
||||||
buf_size : int, optional
|
|
||||||
String buffer size in bytes [default: 256]
|
|
||||||
used when `delim` is specified.
|
|
||||||
bytes : bool, optional
|
|
||||||
If true, will count bytes, ignore `delim`, and default
|
|
||||||
`unit_scale` to True, `unit_divisor` to 1024, and `unit` to 'B'.
|
|
||||||
log : str, optional
|
|
||||||
CRITICAL|FATAL|ERROR|WARN(ING)|[default: 'INFO']|DEBUG|NOTSET.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main(fp=sys.stderr):
|
|
||||||
"""
|
|
||||||
Paramters (internal use only)
|
|
||||||
---------
|
|
||||||
fp : file-like object for tqdm
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
log = sys.argv.index('--log')
|
|
||||||
except ValueError:
|
|
||||||
logLevel = 'INFO'
|
|
||||||
else:
|
|
||||||
# sys.argv.pop(log)
|
|
||||||
# logLevel = sys.argv.pop(log)
|
|
||||||
logLevel = sys.argv[log + 1]
|
|
||||||
logging.basicConfig(level=getattr(logging, logLevel),
|
|
||||||
format="%(levelname)s:%(module)s:%(lineno)d:%(message)s")
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
d = tqdm.__init__.__doc__ + CLI_EXTRA_DOC
|
|
||||||
|
|
||||||
opt_types = dict(RE_OPTS.findall(d))
|
|
||||||
# opt_types['delim'] = 'chr'
|
|
||||||
|
|
||||||
for o in UNSUPPORTED_OPTS:
|
|
||||||
opt_types.pop(o)
|
|
||||||
|
|
||||||
log.debug(sorted(opt_types.items()))
|
|
||||||
|
|
||||||
# d = RE_OPTS.sub(r' --\1=<\1> : \2', d)
|
|
||||||
split = RE_OPTS.split(d)
|
|
||||||
opt_types_desc = zip(split[1::3], split[2::3], split[3::3])
|
|
||||||
d = ''.join('\n --{0}=<{0}> : {1}{2}'.format(*otd)
|
|
||||||
for otd in opt_types_desc if otd[0] not in UNSUPPORTED_OPTS)
|
|
||||||
|
|
||||||
d = """Usage:
|
|
||||||
tqdm [--help | options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help Print this help and exit
|
|
||||||
-v, --version Print version and exit
|
|
||||||
|
|
||||||
""" + d.strip('\n') + '\n'
|
|
||||||
|
|
||||||
# opts = docopt(d, version=__version__)
|
|
||||||
if any(v in sys.argv for v in ('-v', '--version')):
|
|
||||||
sys.stdout.write(__version__ + '\n')
|
|
||||||
sys.exit(0)
|
|
||||||
elif any(v in sys.argv for v in ('-h', '--help')):
|
|
||||||
sys.stdout.write(d + '\n')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
argv = RE_SHLEX.split(' '.join(["tqdm"] + sys.argv[1:]))
|
|
||||||
opts = dict(zip(argv[1::2], argv[2::2]))
|
|
||||||
|
|
||||||
log.debug(opts)
|
|
||||||
opts.pop('log', True)
|
|
||||||
|
|
||||||
tqdm_args = {'file': fp}
|
|
||||||
try:
|
|
||||||
for (o, v) in opts.items():
|
|
||||||
try:
|
|
||||||
tqdm_args[o] = cast(v, opt_types[o])
|
|
||||||
except KeyError as e:
|
|
||||||
raise TqdmKeyError(str(e))
|
|
||||||
log.debug('args:' + str(tqdm_args))
|
|
||||||
except:
|
|
||||||
fp.write('\nError:\nUsage:\n tqdm [--help | options]\n')
|
|
||||||
for i in sys.stdin:
|
|
||||||
sys.stdout.write(i)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
buf_size = tqdm_args.pop('buf_size', 256)
|
|
||||||
delim = tqdm_args.pop('delim', '\n')
|
|
||||||
delim_per_char = tqdm_args.pop('bytes', False)
|
|
||||||
if delim_per_char:
|
|
||||||
tqdm_args.setdefault('unit', 'B')
|
|
||||||
tqdm_args.setdefault('unit_scale', True)
|
|
||||||
tqdm_args.setdefault('unit_divisor', 1024)
|
|
||||||
log.debug(tqdm_args)
|
|
||||||
with tqdm(**tqdm_args) as t:
|
|
||||||
posix_pipe(sys.stdin, sys.stdout,
|
|
||||||
'', buf_size, t.update)
|
|
||||||
elif delim == '\n':
|
|
||||||
log.debug(tqdm_args)
|
|
||||||
for i in tqdm(sys.stdin, **tqdm_args):
|
|
||||||
sys.stdout.write(i)
|
|
||||||
else:
|
|
||||||
log.debug(tqdm_args)
|
|
||||||
with tqdm(**tqdm_args) as t:
|
|
||||||
posix_pipe(sys.stdin, sys.stdout,
|
|
||||||
delim, buf_size, t.update)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
from threading import Event, Thread
|
|
||||||
from time import time
|
|
||||||
from warnings import warn
|
|
||||||
__all__ = ["TMonitor", "TqdmSynchronisationWarning"]
|
|
||||||
|
|
||||||
|
|
||||||
class TqdmSynchronisationWarning(RuntimeWarning):
|
|
||||||
"""tqdm multi-thread/-process errors which may cause incorrect nesting
|
|
||||||
but otherwise no adverse effects"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TMonitor(Thread):
|
|
||||||
"""
|
|
||||||
Monitoring thread for tqdm bars.
|
|
||||||
Monitors if tqdm bars are taking too much time to display
|
|
||||||
and readjusts miniters automatically if necessary.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
tqdm_cls : class
|
|
||||||
tqdm class to use (can be core tqdm or a submodule).
|
|
||||||
sleep_interval : fload
|
|
||||||
Time to sleep between monitoring checks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# internal vars for unit testing
|
|
||||||
_time = None
|
|
||||||
_event = None
|
|
||||||
|
|
||||||
def __init__(self, tqdm_cls, sleep_interval):
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.daemon = True # kill thread when main killed (KeyboardInterrupt)
|
|
||||||
self.was_killed = Event()
|
|
||||||
self.woken = 0 # last time woken up, to sync with monitor
|
|
||||||
self.tqdm_cls = tqdm_cls
|
|
||||||
self.sleep_interval = sleep_interval
|
|
||||||
if TMonitor._time is not None:
|
|
||||||
self._time = TMonitor._time
|
|
||||||
else:
|
|
||||||
self._time = time
|
|
||||||
if TMonitor._event is not None:
|
|
||||||
self._event = TMonitor._event
|
|
||||||
else:
|
|
||||||
self._event = Event
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def exit(self):
|
|
||||||
self.was_killed.set()
|
|
||||||
self.join()
|
|
||||||
return self.report()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
cur_t = self._time()
|
|
||||||
while True:
|
|
||||||
# After processing and before sleeping, notify that we woke
|
|
||||||
# Need to be done just before sleeping
|
|
||||||
self.woken = cur_t
|
|
||||||
# Sleep some time...
|
|
||||||
self.was_killed.wait(self.sleep_interval)
|
|
||||||
# Quit if killed
|
|
||||||
if self.was_killed.is_set():
|
|
||||||
return
|
|
||||||
# Then monitor!
|
|
||||||
# Acquire lock (to access _instances)
|
|
||||||
with self.tqdm_cls.get_lock():
|
|
||||||
cur_t = self._time()
|
|
||||||
# Check tqdm instances are waiting too long to print
|
|
||||||
instances = self.tqdm_cls._instances.copy()
|
|
||||||
for instance in instances:
|
|
||||||
# Check event in loop to reduce blocking time on exit
|
|
||||||
if self.was_killed.is_set():
|
|
||||||
return
|
|
||||||
# Avoid race by checking that the instance started
|
|
||||||
if not hasattr(instance, 'start_t'): # pragma: nocover
|
|
||||||
continue
|
|
||||||
# Only if mininterval > 1 (else iterations are just slow)
|
|
||||||
# and last refresh exceeded maxinterval
|
|
||||||
if instance.miniters > 1 and \
|
|
||||||
(cur_t - instance.last_print_t) >= \
|
|
||||||
instance.maxinterval:
|
|
||||||
# force bypassing miniters on next iteration
|
|
||||||
# (dynamic_miniters adjusts mininterval automatically)
|
|
||||||
instance.miniters = 1
|
|
||||||
# Refresh now! (works only for manual tqdm)
|
|
||||||
instance.refresh(nolock=True)
|
|
||||||
if instances != self.tqdm_cls._instances: # pragma: nocover
|
|
||||||
warn("Set changed size during iteration" +
|
|
||||||
" (see https://github.com/tqdm/tqdm/issues/481)",
|
|
||||||
TqdmSynchronisationWarning)
|
|
||||||
|
|
||||||
def report(self):
|
|
||||||
return not self.was_killed.is_set()
|
|
||||||
1223
lib/tqdm/_tqdm.py
@@ -1,351 +0,0 @@
|
|||||||
"""
|
|
||||||
GUI progressbar decorator for iterators.
|
|
||||||
Includes a default (x)range iterator printing to stderr.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
>>> from tqdm_gui import tgrange[, tqdm_gui]
|
|
||||||
>>> for i in tgrange(10): #same as: for i in tqdm_gui(xrange(10))
|
|
||||||
... ...
|
|
||||||
"""
|
|
||||||
# future division is important to divide integers and get as
|
|
||||||
# a result precise floating numbers (instead of truncated int)
|
|
||||||
from __future__ import division, absolute_import
|
|
||||||
# import compatibility functions and utilities
|
|
||||||
# import sys
|
|
||||||
from time import time
|
|
||||||
from ._utils import _range
|
|
||||||
# to inherit from the tqdm class
|
|
||||||
from ._tqdm import tqdm, TqdmExperimentalWarning
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = {"github.com/": ["casperdcl", "lrq3000"]}
|
|
||||||
__all__ = ['tqdm_gui', 'tgrange']
|
|
||||||
|
|
||||||
|
|
||||||
class tqdm_gui(tqdm): # pragma: no cover
|
|
||||||
"""
|
|
||||||
Experimental GUI version of tqdm!
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO: @classmethod: write() on GUI?
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
import matplotlib as mpl
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from collections import deque
|
|
||||||
kwargs['gui'] = True
|
|
||||||
|
|
||||||
super(tqdm_gui, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Initialize the GUI display
|
|
||||||
if self.disable or not kwargs['gui']:
|
|
||||||
return
|
|
||||||
|
|
||||||
warn('GUI is experimental/alpha', TqdmExperimentalWarning)
|
|
||||||
self.mpl = mpl
|
|
||||||
self.plt = plt
|
|
||||||
self.sp = None
|
|
||||||
|
|
||||||
# Remember if external environment uses toolbars
|
|
||||||
self.toolbar = self.mpl.rcParams['toolbar']
|
|
||||||
self.mpl.rcParams['toolbar'] = 'None'
|
|
||||||
|
|
||||||
self.mininterval = max(self.mininterval, 0.5)
|
|
||||||
self.fig, ax = plt.subplots(figsize=(9, 2.2))
|
|
||||||
# self.fig.subplots_adjust(bottom=0.2)
|
|
||||||
if self.total:
|
|
||||||
self.xdata = []
|
|
||||||
self.ydata = []
|
|
||||||
self.zdata = []
|
|
||||||
else:
|
|
||||||
self.xdata = deque([])
|
|
||||||
self.ydata = deque([])
|
|
||||||
self.zdata = deque([])
|
|
||||||
self.line1, = ax.plot(self.xdata, self.ydata, color='b')
|
|
||||||
self.line2, = ax.plot(self.xdata, self.zdata, color='k')
|
|
||||||
ax.set_ylim(0, 0.001)
|
|
||||||
if self.total:
|
|
||||||
ax.set_xlim(0, 100)
|
|
||||||
ax.set_xlabel('percent')
|
|
||||||
self.fig.legend((self.line1, self.line2), ('cur', 'est'),
|
|
||||||
loc='center right')
|
|
||||||
# progressbar
|
|
||||||
self.hspan = plt.axhspan(0, 0.001,
|
|
||||||
xmin=0, xmax=0, color='g')
|
|
||||||
else:
|
|
||||||
# ax.set_xlim(-60, 0)
|
|
||||||
ax.set_xlim(0, 60)
|
|
||||||
ax.invert_xaxis()
|
|
||||||
ax.set_xlabel('seconds')
|
|
||||||
ax.legend(('cur', 'est'), loc='lower left')
|
|
||||||
ax.grid()
|
|
||||||
# ax.set_xlabel('seconds')
|
|
||||||
ax.set_ylabel((self.unit if self.unit else 'it') + '/s')
|
|
||||||
if self.unit_scale:
|
|
||||||
plt.ticklabel_format(style='sci', axis='y',
|
|
||||||
scilimits=(0, 0))
|
|
||||||
ax.yaxis.get_offset_text().set_x(-0.15)
|
|
||||||
|
|
||||||
# Remember if external environment is interactive
|
|
||||||
self.wasion = plt.isinteractive()
|
|
||||||
plt.ion()
|
|
||||||
self.ax = ax
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
# TODO: somehow allow the following:
|
|
||||||
# if not self.gui:
|
|
||||||
# return super(tqdm_gui, self).__iter__()
|
|
||||||
iterable = self.iterable
|
|
||||||
if self.disable:
|
|
||||||
for obj in iterable:
|
|
||||||
yield obj
|
|
||||||
return
|
|
||||||
|
|
||||||
# ncols = self.ncols
|
|
||||||
mininterval = self.mininterval
|
|
||||||
maxinterval = self.maxinterval
|
|
||||||
miniters = self.miniters
|
|
||||||
dynamic_miniters = self.dynamic_miniters
|
|
||||||
unit = self.unit
|
|
||||||
unit_scale = self.unit_scale
|
|
||||||
ascii = self.ascii
|
|
||||||
start_t = self.start_t
|
|
||||||
last_print_t = self.last_print_t
|
|
||||||
last_print_n = self.last_print_n
|
|
||||||
n = self.n
|
|
||||||
# dynamic_ncols = self.dynamic_ncols
|
|
||||||
smoothing = self.smoothing
|
|
||||||
avg_time = self.avg_time
|
|
||||||
bar_format = self.bar_format
|
|
||||||
|
|
||||||
plt = self.plt
|
|
||||||
ax = self.ax
|
|
||||||
xdata = self.xdata
|
|
||||||
ydata = self.ydata
|
|
||||||
zdata = self.zdata
|
|
||||||
line1 = self.line1
|
|
||||||
line2 = self.line2
|
|
||||||
|
|
||||||
for obj in iterable:
|
|
||||||
yield obj
|
|
||||||
# Update and print the progressbar.
|
|
||||||
# Note: does not call self.update(1) for speed optimisation.
|
|
||||||
n += 1
|
|
||||||
delta_it = n - last_print_n
|
|
||||||
# check the counter first (avoid calls to time())
|
|
||||||
if delta_it >= miniters:
|
|
||||||
cur_t = time()
|
|
||||||
delta_t = cur_t - last_print_t
|
|
||||||
if delta_t >= mininterval:
|
|
||||||
elapsed = cur_t - start_t
|
|
||||||
# EMA (not just overall average)
|
|
||||||
if smoothing and delta_t:
|
|
||||||
avg_time = delta_t / delta_it \
|
|
||||||
if avg_time is None \
|
|
||||||
else smoothing * delta_t / delta_it + \
|
|
||||||
(1 - smoothing) * avg_time
|
|
||||||
|
|
||||||
# Inline due to multiple calls
|
|
||||||
total = self.total
|
|
||||||
# instantaneous rate
|
|
||||||
y = delta_it / delta_t
|
|
||||||
# overall rate
|
|
||||||
z = n / elapsed
|
|
||||||
# update line data
|
|
||||||
xdata.append(n * 100.0 / total if total else cur_t)
|
|
||||||
ydata.append(y)
|
|
||||||
zdata.append(z)
|
|
||||||
|
|
||||||
# Discard old values
|
|
||||||
# xmin, xmax = ax.get_xlim()
|
|
||||||
# if (not total) and elapsed > xmin * 1.1:
|
|
||||||
if (not total) and elapsed > 66:
|
|
||||||
xdata.popleft()
|
|
||||||
ydata.popleft()
|
|
||||||
zdata.popleft()
|
|
||||||
|
|
||||||
ymin, ymax = ax.get_ylim()
|
|
||||||
if y > ymax or z > ymax:
|
|
||||||
ymax = 1.1 * y
|
|
||||||
ax.set_ylim(ymin, ymax)
|
|
||||||
ax.figure.canvas.draw()
|
|
||||||
|
|
||||||
if total:
|
|
||||||
line1.set_data(xdata, ydata)
|
|
||||||
line2.set_data(xdata, zdata)
|
|
||||||
try:
|
|
||||||
poly_lims = self.hspan.get_xy()
|
|
||||||
except AttributeError:
|
|
||||||
self.hspan = plt.axhspan(0, 0.001, xmin=0,
|
|
||||||
xmax=0, color='g')
|
|
||||||
poly_lims = self.hspan.get_xy()
|
|
||||||
poly_lims[0, 1] = ymin
|
|
||||||
poly_lims[1, 1] = ymax
|
|
||||||
poly_lims[2] = [n / total, ymax]
|
|
||||||
poly_lims[3] = [poly_lims[2, 0], ymin]
|
|
||||||
if len(poly_lims) > 4:
|
|
||||||
poly_lims[4, 1] = ymin
|
|
||||||
self.hspan.set_xy(poly_lims)
|
|
||||||
else:
|
|
||||||
t_ago = [cur_t - i for i in xdata]
|
|
||||||
line1.set_data(t_ago, ydata)
|
|
||||||
line2.set_data(t_ago, zdata)
|
|
||||||
|
|
||||||
ax.set_title(self.format_meter(
|
|
||||||
n, total, elapsed, 0,
|
|
||||||
self.desc, ascii, unit, unit_scale,
|
|
||||||
1 / avg_time if avg_time else None, bar_format),
|
|
||||||
fontname="DejaVu Sans Mono", fontsize=11)
|
|
||||||
plt.pause(1e-9)
|
|
||||||
|
|
||||||
# If no `miniters` was specified, adjust automatically
|
|
||||||
# to the maximum iteration rate seen so far.
|
|
||||||
if dynamic_miniters:
|
|
||||||
if maxinterval and delta_t > maxinterval:
|
|
||||||
# Set miniters to correspond to maxinterval
|
|
||||||
miniters = delta_it * maxinterval / delta_t
|
|
||||||
elif mininterval and delta_t:
|
|
||||||
# EMA-weight miniters to converge
|
|
||||||
# towards the timeframe of mininterval
|
|
||||||
miniters = smoothing * delta_it * mininterval \
|
|
||||||
/ delta_t + (1 - smoothing) * miniters
|
|
||||||
else:
|
|
||||||
miniters = smoothing * delta_it + \
|
|
||||||
(1 - smoothing) * miniters
|
|
||||||
|
|
||||||
# Store old values for next call
|
|
||||||
last_print_n = n
|
|
||||||
last_print_t = cur_t
|
|
||||||
|
|
||||||
# Closing the progress bar.
|
|
||||||
# Update some internal variables for close().
|
|
||||||
self.last_print_n = last_print_n
|
|
||||||
self.n = n
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def update(self, n=1):
|
|
||||||
# if not self.gui:
|
|
||||||
# return super(tqdm_gui, self).close()
|
|
||||||
if self.disable:
|
|
||||||
return
|
|
||||||
|
|
||||||
if n < 0:
|
|
||||||
n = 1
|
|
||||||
self.n += n
|
|
||||||
|
|
||||||
delta_it = self.n - self.last_print_n # should be n?
|
|
||||||
if delta_it >= self.miniters:
|
|
||||||
# We check the counter first, to reduce the overhead of time()
|
|
||||||
cur_t = time()
|
|
||||||
delta_t = cur_t - self.last_print_t
|
|
||||||
if delta_t >= self.mininterval:
|
|
||||||
elapsed = cur_t - self.start_t
|
|
||||||
# EMA (not just overall average)
|
|
||||||
if self.smoothing and delta_t:
|
|
||||||
self.avg_time = delta_t / delta_it \
|
|
||||||
if self.avg_time is None \
|
|
||||||
else self.smoothing * delta_t / delta_it + \
|
|
||||||
(1 - self.smoothing) * self.avg_time
|
|
||||||
|
|
||||||
# Inline due to multiple calls
|
|
||||||
total = self.total
|
|
||||||
ax = self.ax
|
|
||||||
|
|
||||||
# instantaneous rate
|
|
||||||
y = delta_it / delta_t
|
|
||||||
# smoothed rate
|
|
||||||
z = self.n / elapsed
|
|
||||||
# update line data
|
|
||||||
self.xdata.append(self.n * 100.0 / total
|
|
||||||
if total else cur_t)
|
|
||||||
self.ydata.append(y)
|
|
||||||
self.zdata.append(z)
|
|
||||||
|
|
||||||
# Discard old values
|
|
||||||
if (not total) and elapsed > 66:
|
|
||||||
self.xdata.popleft()
|
|
||||||
self.ydata.popleft()
|
|
||||||
self.zdata.popleft()
|
|
||||||
|
|
||||||
ymin, ymax = ax.get_ylim()
|
|
||||||
if y > ymax or z > ymax:
|
|
||||||
ymax = 1.1 * y
|
|
||||||
ax.set_ylim(ymin, ymax)
|
|
||||||
ax.figure.canvas.draw()
|
|
||||||
|
|
||||||
if total:
|
|
||||||
self.line1.set_data(self.xdata, self.ydata)
|
|
||||||
self.line2.set_data(self.xdata, self.zdata)
|
|
||||||
try:
|
|
||||||
poly_lims = self.hspan.get_xy()
|
|
||||||
except AttributeError:
|
|
||||||
self.hspan = self.plt.axhspan(0, 0.001, xmin=0,
|
|
||||||
xmax=0, color='g')
|
|
||||||
poly_lims = self.hspan.get_xy()
|
|
||||||
poly_lims[0, 1] = ymin
|
|
||||||
poly_lims[1, 1] = ymax
|
|
||||||
poly_lims[2] = [self.n / total, ymax]
|
|
||||||
poly_lims[3] = [poly_lims[2, 0], ymin]
|
|
||||||
if len(poly_lims) > 4:
|
|
||||||
poly_lims[4, 1] = ymin
|
|
||||||
self.hspan.set_xy(poly_lims)
|
|
||||||
else:
|
|
||||||
t_ago = [cur_t - i for i in self.xdata]
|
|
||||||
self.line1.set_data(t_ago, self.ydata)
|
|
||||||
self.line2.set_data(t_ago, self.zdata)
|
|
||||||
|
|
||||||
ax.set_title(self.format_meter(
|
|
||||||
self.n, total, elapsed, 0,
|
|
||||||
self.desc, self.ascii, self.unit, self.unit_scale,
|
|
||||||
1 / self.avg_time if self.avg_time else None,
|
|
||||||
self.bar_format),
|
|
||||||
fontname="DejaVu Sans Mono", fontsize=11)
|
|
||||||
self.plt.pause(1e-9)
|
|
||||||
|
|
||||||
# If no `miniters` was specified, adjust automatically to the
|
|
||||||
# maximum iteration rate seen so far.
|
|
||||||
# e.g.: After running `tqdm.update(5)`, subsequent
|
|
||||||
# calls to `tqdm.update()` will only cause an update after
|
|
||||||
# at least 5 more iterations.
|
|
||||||
if self.dynamic_miniters:
|
|
||||||
if self.maxinterval and delta_t > self.maxinterval:
|
|
||||||
self.miniters = self.miniters * self.maxinterval \
|
|
||||||
/ delta_t
|
|
||||||
elif self.mininterval and delta_t:
|
|
||||||
self.miniters = self.smoothing * delta_it \
|
|
||||||
* self.mininterval / delta_t + \
|
|
||||||
(1 - self.smoothing) * self.miniters
|
|
||||||
else:
|
|
||||||
self.miniters = self.smoothing * delta_it + \
|
|
||||||
(1 - self.smoothing) * self.miniters
|
|
||||||
|
|
||||||
# Store old values for next call
|
|
||||||
self.last_print_n = self.n
|
|
||||||
self.last_print_t = cur_t
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
# if not self.gui:
|
|
||||||
# return super(tqdm_gui, self).close()
|
|
||||||
if self.disable:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.disable = True
|
|
||||||
|
|
||||||
self._instances.remove(self)
|
|
||||||
|
|
||||||
# Restore toolbars
|
|
||||||
self.mpl.rcParams['toolbar'] = self.toolbar
|
|
||||||
# Return to non-interactive mode
|
|
||||||
if not self.wasion:
|
|
||||||
self.plt.ioff()
|
|
||||||
if not self.leave:
|
|
||||||
self.plt.close(self.fig)
|
|
||||||
|
|
||||||
|
|
||||||
def tgrange(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
A shortcut for tqdm_gui(xrange(*args), **kwargs).
|
|
||||||
On Python3+ range is used instead of xrange.
|
|
||||||
"""
|
|
||||||
return tqdm_gui(_range(*args), **kwargs)
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
"""
|
|
||||||
IPython/Jupyter Notebook progressbar decorator for iterators.
|
|
||||||
Includes a default (x)range iterator printing to stderr.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
>>> from tqdm_notebook import tnrange[, tqdm_notebook]
|
|
||||||
>>> for i in tnrange(10): #same as: for i in tqdm_notebook(xrange(10))
|
|
||||||
... ...
|
|
||||||
"""
|
|
||||||
# future division is important to divide integers and get as
|
|
||||||
# a result precise floating numbers (instead of truncated int)
|
|
||||||
from __future__ import division, absolute_import
|
|
||||||
# import compatibility functions and utilities
|
|
||||||
import sys
|
|
||||||
from ._utils import _range
|
|
||||||
# to inherit from the tqdm class
|
|
||||||
from ._tqdm import tqdm
|
|
||||||
|
|
||||||
|
|
||||||
if True: # pragma: no cover
|
|
||||||
# import IPython/Jupyter base widget and display utilities
|
|
||||||
try: # IPython 4.x
|
|
||||||
import ipywidgets
|
|
||||||
IPY = 4
|
|
||||||
except ImportError: # IPython 3.x / 2.x
|
|
||||||
IPY = 32
|
|
||||||
import warnings
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
ipy_deprecation_msg = "The `IPython.html` package" \
|
|
||||||
" has been deprecated"
|
|
||||||
warnings.filterwarnings('error',
|
|
||||||
message=".*" + ipy_deprecation_msg + ".*")
|
|
||||||
try:
|
|
||||||
import IPython.html.widgets as ipywidgets
|
|
||||||
except Warning as e:
|
|
||||||
if ipy_deprecation_msg not in str(e):
|
|
||||||
raise
|
|
||||||
warnings.simplefilter('ignore')
|
|
||||||
try:
|
|
||||||
import IPython.html.widgets as ipywidgets # NOQA
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try: # IPython 4.x / 3.x
|
|
||||||
if IPY == 32:
|
|
||||||
from IPython.html.widgets import IntProgress, HBox, HTML
|
|
||||||
IPY = 3
|
|
||||||
else:
|
|
||||||
from ipywidgets import IntProgress, HBox, HTML
|
|
||||||
except ImportError:
|
|
||||||
try: # IPython 2.x
|
|
||||||
from IPython.html.widgets import IntProgressWidget as IntProgress
|
|
||||||
from IPython.html.widgets import ContainerWidget as HBox
|
|
||||||
from IPython.html.widgets import HTML
|
|
||||||
IPY = 2
|
|
||||||
except ImportError:
|
|
||||||
IPY = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
from IPython.display import display # , clear_output
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# HTML encoding
|
|
||||||
try: # Py3
|
|
||||||
from html import escape
|
|
||||||
except ImportError: # Py2
|
|
||||||
from cgi import escape
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = {"github.com/": ["lrq3000", "casperdcl", "alexanderkuk"]}
|
|
||||||
__all__ = ['tqdm_notebook', 'tnrange']
|
|
||||||
|
|
||||||
|
|
||||||
class tqdm_notebook(tqdm):
|
|
||||||
"""
|
|
||||||
Experimental IPython/Jupyter Notebook widget using tqdm!
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def status_printer(_, total=None, desc=None):
|
|
||||||
"""
|
|
||||||
Manage the printing of an IPython/Jupyter Notebook progress bar widget.
|
|
||||||
"""
|
|
||||||
# Fallback to text bar if there's no total
|
|
||||||
# DEPRECATED: replaced with an 'info' style bar
|
|
||||||
# if not total:
|
|
||||||
# return super(tqdm_notebook, tqdm_notebook).status_printer(file)
|
|
||||||
|
|
||||||
# fp = file
|
|
||||||
|
|
||||||
# Prepare IPython progress bar
|
|
||||||
if total:
|
|
||||||
pbar = IntProgress(min=0, max=total)
|
|
||||||
else: # No total? Show info style bar with no progress tqdm status
|
|
||||||
pbar = IntProgress(min=0, max=1)
|
|
||||||
pbar.value = 1
|
|
||||||
pbar.bar_style = 'info'
|
|
||||||
if desc:
|
|
||||||
pbar.description = desc
|
|
||||||
# Prepare status text
|
|
||||||
ptext = HTML()
|
|
||||||
# Only way to place text to the right of the bar is to use a container
|
|
||||||
container = HBox(children=[pbar, ptext])
|
|
||||||
display(container)
|
|
||||||
|
|
||||||
def print_status(s='', close=False, bar_style=None, desc=None):
|
|
||||||
# Note: contrary to native tqdm, s='' does NOT clear bar
|
|
||||||
# goal is to keep all infos if error happens so user knows
|
|
||||||
# at which iteration the loop failed.
|
|
||||||
|
|
||||||
# Clear previous output (really necessary?)
|
|
||||||
# clear_output(wait=1)
|
|
||||||
|
|
||||||
# Get current iteration value from format_meter string
|
|
||||||
if total:
|
|
||||||
# n = None
|
|
||||||
if s:
|
|
||||||
npos = s.find(r'/|/') # cause we use bar_format=r'{n}|...'
|
|
||||||
# Check that n can be found in s (else n > total)
|
|
||||||
if npos >= 0:
|
|
||||||
n = int(s[:npos]) # get n from string
|
|
||||||
s = s[npos + 3:] # remove from string
|
|
||||||
|
|
||||||
# Update bar with current n value
|
|
||||||
if n is not None:
|
|
||||||
pbar.value = n
|
|
||||||
|
|
||||||
# Print stats
|
|
||||||
if s: # never clear the bar (signal: s='')
|
|
||||||
s = s.replace('||', '') # remove inesthetical pipes
|
|
||||||
s = escape(s) # html escape special characters (like '?')
|
|
||||||
ptext.value = s
|
|
||||||
|
|
||||||
# Change bar style
|
|
||||||
if bar_style:
|
|
||||||
# Hack-ish way to avoid the danger bar_style being overriden by
|
|
||||||
# success because the bar gets closed after the error...
|
|
||||||
if not (pbar.bar_style == 'danger' and bar_style == 'success'):
|
|
||||||
pbar.bar_style = bar_style
|
|
||||||
|
|
||||||
# Special signal to close the bar
|
|
||||||
if close and pbar.bar_style != 'danger': # hide only if no error
|
|
||||||
try:
|
|
||||||
container.close()
|
|
||||||
except AttributeError:
|
|
||||||
container.visible = False
|
|
||||||
|
|
||||||
# Update description
|
|
||||||
if desc:
|
|
||||||
pbar.description = desc
|
|
||||||
|
|
||||||
return print_status
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# Setup default output
|
|
||||||
if kwargs.get('file', sys.stderr) is sys.stderr:
|
|
||||||
kwargs['file'] = sys.stdout # avoid the red block in IPython
|
|
||||||
|
|
||||||
# Remove the bar from the printed string, only print stats
|
|
||||||
if not kwargs.get('bar_format', None):
|
|
||||||
kwargs['bar_format'] = r'{n}/|/{l_bar}{r_bar}'
|
|
||||||
|
|
||||||
# Initialize parent class + avoid printing by using gui=True
|
|
||||||
kwargs['gui'] = True
|
|
||||||
super(tqdm_notebook, self).__init__(*args, **kwargs)
|
|
||||||
if self.disable or not kwargs['gui']:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Delete first pbar generated from super() (wrong total and text)
|
|
||||||
# DEPRECATED by using gui=True
|
|
||||||
# self.sp('', close=True)
|
|
||||||
# Replace with IPython progress bar display (with correct total)
|
|
||||||
self.sp = self.status_printer(self.fp, self.total, self.desc)
|
|
||||||
self.desc = None # trick to place description before the bar
|
|
||||||
|
|
||||||
# Print initial bar state
|
|
||||||
if not self.disable:
|
|
||||||
self.sp(self.__repr__()) # same as self.refresh without clearing
|
|
||||||
|
|
||||||
def __iter__(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
for obj in super(tqdm_notebook, self).__iter__(*args, **kwargs):
|
|
||||||
# return super(tqdm...) will not catch exception
|
|
||||||
yield obj
|
|
||||||
# NB: except ... [ as ...] breaks IPython async KeyboardInterrupt
|
|
||||||
except:
|
|
||||||
self.sp(bar_style='danger')
|
|
||||||
raise
|
|
||||||
|
|
||||||
def update(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
super(tqdm_notebook, self).update(*args, **kwargs)
|
|
||||||
except Exception as exc:
|
|
||||||
# cannot catch KeyboardInterrupt when using manual tqdm
|
|
||||||
# as the interrupt will most likely happen on another statement
|
|
||||||
self.sp(bar_style='danger')
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def close(self, *args, **kwargs):
|
|
||||||
super(tqdm_notebook, self).close(*args, **kwargs)
|
|
||||||
# If it was not run in a notebook, sp is not assigned, check for it
|
|
||||||
if hasattr(self, 'sp'):
|
|
||||||
# Try to detect if there was an error or KeyboardInterrupt
|
|
||||||
# in manual mode: if n < total, things probably got wrong
|
|
||||||
if self.total and self.n < self.total:
|
|
||||||
self.sp(bar_style='danger')
|
|
||||||
else:
|
|
||||||
if self.leave:
|
|
||||||
self.sp(bar_style='success')
|
|
||||||
else:
|
|
||||||
self.sp(close=True)
|
|
||||||
|
|
||||||
def moveto(self, *args, **kwargs):
|
|
||||||
# void -> avoid extraneous `\n` in IPython output cell
|
|
||||||
return
|
|
||||||
|
|
||||||
def set_description(self, desc=None, **_):
|
|
||||||
"""
|
|
||||||
Set/modify description of the progress bar.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
desc : str, optional
|
|
||||||
"""
|
|
||||||
self.sp(desc=desc)
|
|
||||||
|
|
||||||
|
|
||||||
def tnrange(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
A shortcut for tqdm_notebook(xrange(*args), **kwargs).
|
|
||||||
On Python3+ range is used instead of xrange.
|
|
||||||
"""
|
|
||||||
return tqdm_notebook(_range(*args), **kwargs)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
__author__ = "github.com/casperdcl"
|
|
||||||
__all__ = ['tqdm_pandas']
|
|
||||||
|
|
||||||
|
|
||||||
def tqdm_pandas(tclass, *targs, **tkwargs):
|
|
||||||
"""
|
|
||||||
Registers the given `tqdm` instance with
|
|
||||||
`pandas.core.groupby.DataFrameGroupBy.progress_apply`.
|
|
||||||
It will even close() the `tqdm` instance upon completion.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
tclass : tqdm class you want to use (eg, tqdm, tqdm_notebook, etc)
|
|
||||||
targs and tkwargs : arguments for the tqdm instance
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
>>> import pandas as pd
|
|
||||||
>>> import numpy as np
|
|
||||||
>>> from tqdm import tqdm, tqdm_pandas
|
|
||||||
>>>
|
|
||||||
>>> df = pd.DataFrame(np.random.randint(0, 100, (100000, 6)))
|
|
||||||
>>> tqdm_pandas(tqdm, leave=True) # can use tqdm_gui, optional kwargs, etc
|
|
||||||
>>> # Now you can use `progress_apply` instead of `apply`
|
|
||||||
>>> df.groupby(0).progress_apply(lambda x: x**2)
|
|
||||||
|
|
||||||
References
|
|
||||||
----------
|
|
||||||
https://stackoverflow.com/questions/18603270/
|
|
||||||
progress-indicator-during-pandas-operations-python
|
|
||||||
"""
|
|
||||||
from tqdm import TqdmDeprecationWarning
|
|
||||||
|
|
||||||
if isinstance(tclass, type) or (getattr(tclass, '__name__', '').startswith(
|
|
||||||
'tqdm_')): # delayed adapter case
|
|
||||||
TqdmDeprecationWarning("""\
|
|
||||||
Please use `tqdm.pandas(...)` instead of `tqdm_pandas(tqdm, ...)`.
|
|
||||||
""", fp_write=getattr(tkwargs.get('file', None), 'write', sys.stderr.write))
|
|
||||||
tclass.pandas(*targs, **tkwargs)
|
|
||||||
else:
|
|
||||||
TqdmDeprecationWarning("""\
|
|
||||||
Please use `tqdm.pandas(...)` instead of `tqdm_pandas(tqdm(...))`.
|
|
||||||
""", fp_write=getattr(tclass.fp, 'write', sys.stderr.write))
|
|
||||||
type(tclass).pandas(deprecated_t=tclass)
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from platform import system as _curos
|
|
||||||
CUR_OS = _curos()
|
|
||||||
IS_WIN = CUR_OS in ['Windows', 'cli']
|
|
||||||
IS_NIX = (not IS_WIN) and any(
|
|
||||||
CUR_OS.startswith(i) for i in
|
|
||||||
['CYGWIN', 'MSYS', 'Linux', 'Darwin', 'SunOS', 'FreeBSD', 'NetBSD'])
|
|
||||||
|
|
||||||
|
|
||||||
# Py2/3 compat. Empty conditional to avoid coverage
|
|
||||||
if True: # pragma: no cover
|
|
||||||
try:
|
|
||||||
_range = xrange
|
|
||||||
except NameError:
|
|
||||||
_range = range
|
|
||||||
|
|
||||||
try:
|
|
||||||
_unich = unichr
|
|
||||||
except NameError:
|
|
||||||
_unich = chr
|
|
||||||
|
|
||||||
try:
|
|
||||||
_unicode = unicode
|
|
||||||
except NameError:
|
|
||||||
_unicode = str
|
|
||||||
|
|
||||||
try:
|
|
||||||
if IS_WIN:
|
|
||||||
import colorama
|
|
||||||
colorama.init()
|
|
||||||
else:
|
|
||||||
colorama = None
|
|
||||||
except ImportError:
|
|
||||||
colorama = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from weakref import WeakSet
|
|
||||||
except ImportError:
|
|
||||||
WeakSet = set
|
|
||||||
|
|
||||||
try:
|
|
||||||
_basestring = basestring
|
|
||||||
except NameError:
|
|
||||||
_basestring = str
|
|
||||||
|
|
||||||
try: # py>=2.7,>=3.1
|
|
||||||
from collections import OrderedDict as _OrderedDict
|
|
||||||
except ImportError:
|
|
||||||
try: # older Python versions with backported ordereddict lib
|
|
||||||
from ordereddict import OrderedDict as _OrderedDict
|
|
||||||
except ImportError: # older Python versions without ordereddict lib
|
|
||||||
# Py2.6,3.0 compat, from PEP 372
|
|
||||||
from collections import MutableMapping
|
|
||||||
|
|
||||||
class _OrderedDict(dict, MutableMapping):
|
|
||||||
# Methods with direct access to underlying attributes
|
|
||||||
def __init__(self, *args, **kwds):
|
|
||||||
if len(args) > 1:
|
|
||||||
raise TypeError('expected at 1 argument, got %d',
|
|
||||||
len(args))
|
|
||||||
if not hasattr(self, '_keys'):
|
|
||||||
self._keys = []
|
|
||||||
self.update(*args, **kwds)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
del self._keys[:]
|
|
||||||
dict.clear(self)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if key not in self:
|
|
||||||
self._keys.append(key)
|
|
||||||
dict.__setitem__(self, key, value)
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
dict.__delitem__(self, key)
|
|
||||||
self._keys.remove(key)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._keys)
|
|
||||||
|
|
||||||
def __reversed__(self):
|
|
||||||
return reversed(self._keys)
|
|
||||||
|
|
||||||
def popitem(self):
|
|
||||||
if not self:
|
|
||||||
raise KeyError
|
|
||||||
key = self._keys.pop()
|
|
||||||
value = dict.pop(self, key)
|
|
||||||
return key, value
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
items = [[k, self[k]] for k in self]
|
|
||||||
inst_dict = vars(self).copy()
|
|
||||||
inst_dict.pop('_keys', None)
|
|
||||||
return self.__class__, (items,), inst_dict
|
|
||||||
|
|
||||||
# Methods with indirect access via the above methods
|
|
||||||
setdefault = MutableMapping.setdefault
|
|
||||||
update = MutableMapping.update
|
|
||||||
pop = MutableMapping.pop
|
|
||||||
keys = MutableMapping.keys
|
|
||||||
values = MutableMapping.values
|
|
||||||
items = MutableMapping.items
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
pairs = ', '.join(map('%r: %r'.__mod__, self.items()))
|
|
||||||
return '%s({%s})' % (self.__class__.__name__, pairs)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self.__class__(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromkeys(cls, iterable, value=None):
|
|
||||||
d = cls()
|
|
||||||
for key in iterable:
|
|
||||||
d[key] = value
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def _is_utf(encoding):
|
|
||||||
try:
|
|
||||||
u'\u2588\u2589'.encode(encoding)
|
|
||||||
except UnicodeEncodeError: # pragma: no cover
|
|
||||||
return False
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
try:
|
|
||||||
return encoding.lower().startswith('utf-') or ('U8' == encoding)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _supports_unicode(fp):
|
|
||||||
try:
|
|
||||||
return _is_utf(fp.encoding)
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _environ_cols_wrapper(): # pragma: no cover
|
|
||||||
"""
|
|
||||||
Return a function which gets width and height of console
|
|
||||||
(linux,osx,windows,cygwin).
|
|
||||||
"""
|
|
||||||
_environ_cols = None
|
|
||||||
if IS_WIN:
|
|
||||||
_environ_cols = _environ_cols_windows
|
|
||||||
if _environ_cols is None:
|
|
||||||
_environ_cols = _environ_cols_tput
|
|
||||||
if IS_NIX:
|
|
||||||
_environ_cols = _environ_cols_linux
|
|
||||||
return _environ_cols
|
|
||||||
|
|
||||||
|
|
||||||
def _environ_cols_windows(fp): # pragma: no cover
|
|
||||||
try:
|
|
||||||
from ctypes import windll, create_string_buffer
|
|
||||||
import struct
|
|
||||||
from sys import stdin, stdout
|
|
||||||
|
|
||||||
io_handle = -12 # assume stderr
|
|
||||||
if fp == stdin:
|
|
||||||
io_handle = -10
|
|
||||||
elif fp == stdout:
|
|
||||||
io_handle = -11
|
|
||||||
|
|
||||||
h = windll.kernel32.GetStdHandle(io_handle)
|
|
||||||
csbi = create_string_buffer(22)
|
|
||||||
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
|
|
||||||
if res:
|
|
||||||
(_bufx, _bufy, _curx, _cury, _wattr, left, _top, right, _bottom,
|
|
||||||
_maxx, _maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
|
|
||||||
# nlines = bottom - top + 1
|
|
||||||
return right - left # +1
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _environ_cols_tput(*_): # pragma: no cover
|
|
||||||
"""cygwin xterm (windows)"""
|
|
||||||
try:
|
|
||||||
import shlex
|
|
||||||
cols = int(subprocess.check_call(shlex.split('tput cols')))
|
|
||||||
# rows = int(subprocess.check_call(shlex.split('tput lines')))
|
|
||||||
return cols
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _environ_cols_linux(fp): # pragma: no cover
|
|
||||||
|
|
||||||
try:
|
|
||||||
from termios import TIOCGWINSZ
|
|
||||||
from fcntl import ioctl
|
|
||||||
from array import array
|
|
||||||
except ImportError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return array('h', ioctl(fp, TIOCGWINSZ, '\0' * 8))[1]
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
from os.environ import get
|
|
||||||
except ImportError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return int(get('COLUMNS', 1)) - 1
|
|
||||||
|
|
||||||
|
|
||||||
def _term_move_up(): # pragma: no cover
|
|
||||||
return '' if (os.name == 'nt') and (colorama is None) else '\x1b[A'
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# Definition of the version number
|
|
||||||
import os
|
|
||||||
from io import open as io_open
|
|
||||||
|
|
||||||
__all__ = ["__version__"]
|
|
||||||
|
|
||||||
# major, minor, patch, -extra
|
|
||||||
version_info = 4, 21, 0
|
|
||||||
|
|
||||||
# Nice string for the version
|
|
||||||
__version__ = '.'.join(map(str, version_info))
|
|
||||||
|
|
||||||
|
|
||||||
# auto -extra based on commit hash (if not tagged as release)
|
|
||||||
scriptdir = os.path.dirname(__file__)
|
|
||||||
gitdir = os.path.abspath(os.path.join(scriptdir, "..", ".git"))
|
|
||||||
if os.path.isdir(gitdir): # pragma: nocover
|
|
||||||
extra = None
|
|
||||||
# Open config file to check if we are in tqdm project
|
|
||||||
with io_open(os.path.join(gitdir, "config"), 'r') as fh_config:
|
|
||||||
if 'tqdm' in fh_config.read():
|
|
||||||
# Open the HEAD file
|
|
||||||
with io_open(os.path.join(gitdir, "HEAD"), 'r') as fh_head:
|
|
||||||
extra = fh_head.readline().strip()
|
|
||||||
# in a branch => HEAD points to file containing last commit
|
|
||||||
if 'ref:' in extra:
|
|
||||||
# reference file path
|
|
||||||
ref_file = extra[5:]
|
|
||||||
branch_name = ref_file.rsplit('/', 1)[-1]
|
|
||||||
|
|
||||||
ref_file_path = os.path.abspath(os.path.join(gitdir, ref_file))
|
|
||||||
# check that we are in git folder
|
|
||||||
# (by stripping the git folder from the ref file path)
|
|
||||||
if os.path.relpath(
|
|
||||||
ref_file_path, gitdir).replace('\\', '/') != ref_file:
|
|
||||||
# out of git folder
|
|
||||||
extra = None
|
|
||||||
else:
|
|
||||||
# open the ref file
|
|
||||||
with io_open(ref_file_path, 'r') as fh_branch:
|
|
||||||
commit_hash = fh_branch.readline().strip()
|
|
||||||
extra = commit_hash[:8]
|
|
||||||
if branch_name != "master":
|
|
||||||
extra += '.' + branch_name
|
|
||||||
|
|
||||||
# detached HEAD mode, already have commit hash
|
|
||||||
else:
|
|
||||||
extra = extra[:8]
|
|
||||||
|
|
||||||
# Append commit hash (and branch) to version string if not tagged
|
|
||||||
if extra is not None:
|
|
||||||
try:
|
|
||||||
with io_open(os.path.join(gitdir, "refs", "tags",
|
|
||||||
'v' + __version__)) as fdv:
|
|
||||||
if fdv.readline().strip()[:8] != extra[:8]:
|
|
||||||
__version__ += '-' + extra
|
|
||||||
except Exception as e:
|
|
||||||
if "No such file" not in str(e):
|
|
||||||
raise
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from tqdm import main, TqdmKeyError, TqdmTypeError
|
|
||||||
|
|
||||||
from tests_tqdm import with_setup, pretest, posttest, _range, closing, \
|
|
||||||
UnicodeIO, StringIO
|
|
||||||
|
|
||||||
|
|
||||||
def _sh(*cmd, **kwargs):
|
|
||||||
return subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
|
||||||
**kwargs).communicate()[0].decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
# WARNING: this should be the last test as it messes with sys.stdin, argv
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_main():
|
|
||||||
"""Test command line pipes"""
|
|
||||||
ls_out = _sh('ls').replace('\r\n', '\n')
|
|
||||||
ls = subprocess.Popen('ls', stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT)
|
|
||||||
res = _sh(sys.executable, '-c', 'from tqdm import main; main()',
|
|
||||||
stdin=ls.stdout, stderr=subprocess.STDOUT)
|
|
||||||
ls.wait()
|
|
||||||
|
|
||||||
# actual test:
|
|
||||||
|
|
||||||
assert (ls_out in res.replace('\r\n', '\n'))
|
|
||||||
|
|
||||||
# semi-fake test which gets coverage:
|
|
||||||
_SYS = sys.stdin, sys.argv
|
|
||||||
|
|
||||||
with closing(StringIO()) as sys.stdin:
|
|
||||||
sys.argv = ['', '--desc', 'Test CLI-delims',
|
|
||||||
'--ascii', 'True', '--delim', r'\0', '--buf_size', '64']
|
|
||||||
sys.stdin.write('\0'.join(map(str, _range(int(1e3)))))
|
|
||||||
sys.stdin.seek(0)
|
|
||||||
main()
|
|
||||||
|
|
||||||
IN_DATA_LIST = map(str, _range(int(1e3)))
|
|
||||||
sys.stdin = IN_DATA_LIST
|
|
||||||
sys.argv = ['', '--desc', 'Test CLI pipes',
|
|
||||||
'--ascii', 'True', '--unit_scale', 'True']
|
|
||||||
import tqdm.__main__ # NOQA
|
|
||||||
|
|
||||||
IN_DATA = '\0'.join(IN_DATA_LIST)
|
|
||||||
with closing(StringIO()) as sys.stdin:
|
|
||||||
sys.stdin.write(IN_DATA)
|
|
||||||
sys.stdin.seek(0)
|
|
||||||
sys.argv = ['', '--ascii', '--bytes', '--unit_scale', 'False']
|
|
||||||
with closing(UnicodeIO()) as fp:
|
|
||||||
main(fp=fp)
|
|
||||||
assert (str(len(IN_DATA)) in fp.getvalue())
|
|
||||||
|
|
||||||
sys.stdin = IN_DATA_LIST
|
|
||||||
sys.argv = ['', '-ascii', '--unit_scale', 'False',
|
|
||||||
'--desc', 'Test CLI errors']
|
|
||||||
main()
|
|
||||||
|
|
||||||
sys.argv = ['', '-ascii', '-unit_scale', '--bad_arg_u_ment', 'foo']
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except TqdmKeyError as e:
|
|
||||||
if 'bad_arg_u_ment' not in str(e):
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
raise TqdmKeyError('bad_arg_u_ment')
|
|
||||||
|
|
||||||
sys.argv = ['', '-ascii', '-unit_scale', 'invalid_bool_value']
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except TqdmTypeError as e:
|
|
||||||
if 'invalid_bool_value' not in str(e):
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
raise TqdmTypeError('invalid_bool_value')
|
|
||||||
|
|
||||||
sys.argv = ['', '-ascii', '--total', 'invalid_int_value']
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except TqdmTypeError as e:
|
|
||||||
if 'invalid_int_value' not in str(e):
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
raise TqdmTypeError('invalid_int_value')
|
|
||||||
|
|
||||||
for i in ('-h', '--help', '-v', '--version'):
|
|
||||||
sys.argv = ['', i]
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except SystemExit:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# clean up
|
|
||||||
sys.stdin, sys.argv = _SYS
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
from nose.plugins.skip import SkipTest
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
from tests_tqdm import with_setup, pretest, posttest, StringIO, closing
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_pandas_series():
|
|
||||||
"""Test pandas.Series.progress_apply and .progress_map"""
|
|
||||||
try:
|
|
||||||
from numpy.random import randint
|
|
||||||
import pandas as pd
|
|
||||||
except ImportError:
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm.pandas(file=our_file, leave=True, ascii=True)
|
|
||||||
|
|
||||||
series = pd.Series(randint(0, 50, (123,)))
|
|
||||||
res1 = series.progress_apply(lambda x: x + 10)
|
|
||||||
res2 = series.apply(lambda x: x + 10)
|
|
||||||
assert res1.equals(res2)
|
|
||||||
|
|
||||||
res3 = series.progress_map(lambda x: x + 10)
|
|
||||||
res4 = series.map(lambda x: x + 10)
|
|
||||||
assert res3.equals(res4)
|
|
||||||
|
|
||||||
expects = ['100%', '123/123']
|
|
||||||
for exres in expects:
|
|
||||||
our_file.seek(0)
|
|
||||||
if our_file.getvalue().count(exres) < 2:
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError(
|
|
||||||
"\nExpected:\n{0}\nIn:\n{1}\n".format(
|
|
||||||
exres + " at least twice.", our_file.read()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_pandas_data_frame():
|
|
||||||
"""Test pandas.DataFrame.progress_apply and .progress_applymap"""
|
|
||||||
try:
|
|
||||||
from numpy.random import randint
|
|
||||||
import pandas as pd
|
|
||||||
except ImportError:
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm.pandas(file=our_file, leave=True, ascii=True)
|
|
||||||
df = pd.DataFrame(randint(0, 50, (100, 200)))
|
|
||||||
|
|
||||||
def task_func(x):
|
|
||||||
return x + 1
|
|
||||||
|
|
||||||
# applymap
|
|
||||||
res1 = df.progress_applymap(task_func)
|
|
||||||
res2 = df.applymap(task_func)
|
|
||||||
assert res1.equals(res2)
|
|
||||||
|
|
||||||
# apply
|
|
||||||
for axis in [0, 1]:
|
|
||||||
res3 = df.progress_apply(task_func, axis=axis)
|
|
||||||
res4 = df.apply(task_func, axis=axis)
|
|
||||||
assert res3.equals(res4)
|
|
||||||
|
|
||||||
our_file.seek(0)
|
|
||||||
if our_file.read().count('100%') < 3:
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError("\nExpected:\n{0}\nIn:\n{1}\n".format(
|
|
||||||
'100% at least three times', our_file.read()))
|
|
||||||
|
|
||||||
# apply_map, apply axis=0, apply axis=1
|
|
||||||
expects = ['20000/20000', '200/200', '100/100']
|
|
||||||
for exres in expects:
|
|
||||||
our_file.seek(0)
|
|
||||||
if our_file.getvalue().count(exres) < 1:
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError(
|
|
||||||
"\nExpected:\n{0}\nIn:\n {1}\n".format(
|
|
||||||
exres + " at least once.", our_file.read()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_pandas_groupby_apply():
|
|
||||||
"""Test pandas.DataFrame.groupby(...).progress_apply"""
|
|
||||||
try:
|
|
||||||
from numpy.random import randint
|
|
||||||
import pandas as pd
|
|
||||||
except ImportError:
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm.pandas(file=our_file, leave=False, ascii=True)
|
|
||||||
|
|
||||||
df = pd.DataFrame(randint(0, 50, (500, 3)))
|
|
||||||
df.groupby(0).progress_apply(lambda x: None)
|
|
||||||
|
|
||||||
dfs = pd.DataFrame(randint(0, 50, (500, 3)), columns=list('abc'))
|
|
||||||
dfs.groupby(['a']).progress_apply(lambda x: None)
|
|
||||||
|
|
||||||
our_file.seek(0)
|
|
||||||
|
|
||||||
# don't expect final output since no `leave` and
|
|
||||||
# high dynamic `miniters`
|
|
||||||
nexres = '100%|##########|'
|
|
||||||
if nexres in our_file.read():
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError("\nDid not expect:\n{0}\nIn:{1}\n".format(
|
|
||||||
nexres, our_file.read()))
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm.pandas(file=our_file, leave=True, ascii=True)
|
|
||||||
|
|
||||||
dfs = pd.DataFrame(randint(0, 50, (500, 3)), columns=list('abc'))
|
|
||||||
dfs.loc[0] = [2, 1, 1]
|
|
||||||
dfs['d'] = 100
|
|
||||||
|
|
||||||
expects = ['500/500', '1/1', '4/4', '2/2']
|
|
||||||
dfs.groupby(dfs.index).progress_apply(lambda x: None)
|
|
||||||
dfs.groupby('d').progress_apply(lambda x: None)
|
|
||||||
dfs.groupby(dfs.columns, axis=1).progress_apply(lambda x: None)
|
|
||||||
dfs.groupby([2, 2, 1, 1], axis=1).progress_apply(lambda x: None)
|
|
||||||
|
|
||||||
our_file.seek(0)
|
|
||||||
if our_file.read().count('100%') < 4:
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError("\nExpected:\n{0}\nIn:\n{1}\n".format(
|
|
||||||
'100% at least four times', our_file.read()))
|
|
||||||
|
|
||||||
for exres in expects:
|
|
||||||
our_file.seek(0)
|
|
||||||
if our_file.getvalue().count(exres) < 1:
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError(
|
|
||||||
"\nExpected:\n{0}\nIn:\n {1}\n".format(
|
|
||||||
exres + " at least once.", our_file.read()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_pandas_leave():
|
|
||||||
"""Test pandas with `leave=True`"""
|
|
||||||
try:
|
|
||||||
from numpy.random import randint
|
|
||||||
import pandas as pd
|
|
||||||
except ImportError:
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
df = pd.DataFrame(randint(0, 100, (1000, 6)))
|
|
||||||
tqdm.pandas(file=our_file, leave=True, ascii=True)
|
|
||||||
df.groupby(0).progress_apply(lambda x: None)
|
|
||||||
|
|
||||||
our_file.seek(0)
|
|
||||||
|
|
||||||
exres = '100%|##########| 100/100'
|
|
||||||
if exres not in our_file.read():
|
|
||||||
our_file.seek(0)
|
|
||||||
raise AssertionError(
|
|
||||||
"\nExpected:\n{0}\nIn:{1}\n".format(exres, our_file.read()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_pandas_apply_args_deprecation():
|
|
||||||
"""Test warning info in
|
|
||||||
`pandas.Dataframe(Series).progress_apply(func, *args)`"""
|
|
||||||
try:
|
|
||||||
from numpy.random import randint
|
|
||||||
from tqdm import tqdm_pandas
|
|
||||||
import pandas as pd
|
|
||||||
except ImportError:
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm_pandas(tqdm(file=our_file, leave=False, ascii=True, ncols=20))
|
|
||||||
df = pd.DataFrame(randint(0, 50, (500, 3)))
|
|
||||||
df.progress_apply(lambda x: None, 1) # 1 shall cause a warning
|
|
||||||
# Check deprecation message
|
|
||||||
res = our_file.getvalue()
|
|
||||||
assert all([i in res for i in (
|
|
||||||
"TqdmDeprecationWarning", "not supported",
|
|
||||||
"keyword arguments instead")])
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
def test_pandas_deprecation():
|
|
||||||
"""Test bar object instance as argument deprecation"""
|
|
||||||
try:
|
|
||||||
from numpy.random import randint
|
|
||||||
from tqdm import tqdm_pandas
|
|
||||||
import pandas as pd
|
|
||||||
except ImportError:
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm_pandas(tqdm(file=our_file, leave=False, ascii=True, ncols=20))
|
|
||||||
df = pd.DataFrame(randint(0, 50, (500, 3)))
|
|
||||||
df.groupby(0).progress_apply(lambda x: None)
|
|
||||||
# Check deprecation message
|
|
||||||
assert "TqdmDeprecationWarning" in our_file.getvalue()
|
|
||||||
assert "instead of `tqdm_pandas(tqdm(...))`" in our_file.getvalue()
|
|
||||||
|
|
||||||
with closing(StringIO()) as our_file:
|
|
||||||
tqdm_pandas(tqdm, file=our_file, leave=False, ascii=True, ncols=20)
|
|
||||||
df = pd.DataFrame(randint(0, 50, (500, 3)))
|
|
||||||
df.groupby(0).progress_apply(lambda x: None)
|
|
||||||
# Check deprecation message
|
|
||||||
assert "TqdmDeprecationWarning" in our_file.getvalue()
|
|
||||||
assert "instead of `tqdm_pandas(tqdm, ...)`" in our_file.getvalue()
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
from __future__ import print_function, division
|
|
||||||
|
|
||||||
from nose.plugins.skip import SkipTest
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from time import sleep, time
|
|
||||||
|
|
||||||
from tqdm import trange
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from tests_tqdm import with_setup, pretest, posttest, StringIO, closing, _range
|
|
||||||
|
|
||||||
# Use relative/cpu timer to have reliable timings when there is a sudden load
|
|
||||||
try:
|
|
||||||
from time import process_time
|
|
||||||
except ImportError:
|
|
||||||
from time import clock
|
|
||||||
process_time = clock
|
|
||||||
|
|
||||||
|
|
||||||
def get_relative_time(prevtime=0):
|
|
||||||
return process_time() - prevtime
|
|
||||||
|
|
||||||
|
|
||||||
def cpu_sleep(t):
|
|
||||||
"""Sleep the given amount of cpu time"""
|
|
||||||
start = process_time()
|
|
||||||
while (process_time() - start) < t:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def checkCpuTime(sleeptime=0.2):
|
|
||||||
"""Check if cpu time works correctly"""
|
|
||||||
if checkCpuTime.passed:
|
|
||||||
return True
|
|
||||||
# First test that sleeping does not consume cputime
|
|
||||||
start1 = process_time()
|
|
||||||
sleep(sleeptime)
|
|
||||||
t1 = process_time() - start1
|
|
||||||
|
|
||||||
# secondly check by comparing to cpusleep (where we actually do something)
|
|
||||||
start2 = process_time()
|
|
||||||
cpu_sleep(sleeptime)
|
|
||||||
t2 = process_time() - start2
|
|
||||||
|
|
||||||
if abs(t1) < 0.0001 and (t1 < t2 / 10):
|
|
||||||
return True
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
|
|
||||||
checkCpuTime.passed = False
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def relative_timer():
|
|
||||||
start = process_time()
|
|
||||||
|
|
||||||
def elapser():
|
|
||||||
return process_time() - start
|
|
||||||
|
|
||||||
yield lambda: elapser()
|
|
||||||
spent = process_time() - start
|
|
||||||
|
|
||||||
def elapser(): # NOQA
|
|
||||||
return spent
|
|
||||||
|
|
||||||
|
|
||||||
def retry_on_except(n=3):
|
|
||||||
def wrapper(fn):
|
|
||||||
def test_inner():
|
|
||||||
for i in range(1, n + 1):
|
|
||||||
try:
|
|
||||||
checkCpuTime()
|
|
||||||
fn()
|
|
||||||
except SkipTest:
|
|
||||||
if i >= n:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
test_inner.__doc__ = fn.__doc__
|
|
||||||
return test_inner
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class MockIO(StringIO):
|
|
||||||
"""Wraps StringIO to mock a file with no I/O"""
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def simple_progress(iterable=None, total=None, file=sys.stdout, desc='',
|
|
||||||
leave=False, miniters=1, mininterval=0.1, width=60):
|
|
||||||
"""Simple progress bar reproducing tqdm's major features"""
|
|
||||||
n = [0] # use a closure
|
|
||||||
start_t = [time()]
|
|
||||||
last_n = [0]
|
|
||||||
last_t = [0]
|
|
||||||
if iterable is not None:
|
|
||||||
total = len(iterable)
|
|
||||||
|
|
||||||
def format_interval(t):
|
|
||||||
mins, s = divmod(int(t), 60)
|
|
||||||
h, m = divmod(mins, 60)
|
|
||||||
if h:
|
|
||||||
return '{0:d}:{1:02d}:{2:02d}'.format(h, m, s)
|
|
||||||
else:
|
|
||||||
return '{0:02d}:{1:02d}'.format(m, s)
|
|
||||||
|
|
||||||
def update_and_print(i=1):
|
|
||||||
n[0] += i
|
|
||||||
if (n[0] - last_n[0]) >= miniters:
|
|
||||||
last_n[0] = n[0]
|
|
||||||
|
|
||||||
if (time() - last_t[0]) >= mininterval:
|
|
||||||
last_t[0] = time() # last_t[0] == current time
|
|
||||||
|
|
||||||
spent = last_t[0] - start_t[0]
|
|
||||||
spent_fmt = format_interval(spent)
|
|
||||||
rate = n[0] / spent if spent > 0 else 0
|
|
||||||
if 0.0 < rate < 1.0:
|
|
||||||
rate_fmt = "%.2fs/it" % (1.0 / rate)
|
|
||||||
else:
|
|
||||||
rate_fmt = "%.2fit/s" % rate
|
|
||||||
|
|
||||||
frac = n[0] / total
|
|
||||||
percentage = int(frac * 100)
|
|
||||||
eta = (total - n[0]) / rate if rate > 0 else 0
|
|
||||||
eta_fmt = format_interval(eta)
|
|
||||||
|
|
||||||
# bar = "#" * int(frac * width)
|
|
||||||
barfill = " " * int((1.0 - frac) * width)
|
|
||||||
bar_length, frac_bar_length = divmod(int(frac * width * 10), 10)
|
|
||||||
bar = '#' * bar_length
|
|
||||||
frac_bar = chr(48 + frac_bar_length) if frac_bar_length \
|
|
||||||
else ' '
|
|
||||||
|
|
||||||
file.write("\r%s %i%%|%s%s%s| %i/%i [%s<%s, %s]" %
|
|
||||||
(desc, percentage, bar, frac_bar, barfill, n[0],
|
|
||||||
total, spent_fmt, eta_fmt, rate_fmt))
|
|
||||||
|
|
||||||
if n[0] == total and leave:
|
|
||||||
file.write("\n")
|
|
||||||
file.flush()
|
|
||||||
|
|
||||||
def update_and_yield():
|
|
||||||
for elt in iterable:
|
|
||||||
yield elt
|
|
||||||
update_and_print()
|
|
||||||
|
|
||||||
update_and_print(0)
|
|
||||||
if iterable is not None:
|
|
||||||
return update_and_yield()
|
|
||||||
else:
|
|
||||||
return update_and_print
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
@retry_on_except()
|
|
||||||
def test_iter_overhead():
|
|
||||||
"""Test overhead of iteration based tqdm"""
|
|
||||||
|
|
||||||
total = int(1e6)
|
|
||||||
|
|
||||||
with closing(MockIO()) as our_file:
|
|
||||||
a = 0
|
|
||||||
with trange(total, file=our_file) as t:
|
|
||||||
with relative_timer() as time_tqdm:
|
|
||||||
for i in t:
|
|
||||||
a += i
|
|
||||||
assert (a == (total * total - total) / 2.0)
|
|
||||||
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_bench:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
our_file.write(a)
|
|
||||||
|
|
||||||
# Compute relative overhead of tqdm against native range()
|
|
||||||
if time_tqdm() > 9 * time_bench():
|
|
||||||
raise AssertionError('trange(%g): %f, range(%g): %f' %
|
|
||||||
(total, time_tqdm(), total, time_bench()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
@retry_on_except()
|
|
||||||
def test_manual_overhead():
|
|
||||||
"""Test overhead of manual tqdm"""
|
|
||||||
|
|
||||||
total = int(1e6)
|
|
||||||
|
|
||||||
with closing(MockIO()) as our_file:
|
|
||||||
with tqdm(total=total * 10, file=our_file, leave=True) as t:
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_tqdm:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
t.update(10)
|
|
||||||
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_bench:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
our_file.write(a)
|
|
||||||
|
|
||||||
# Compute relative overhead of tqdm against native range()
|
|
||||||
if time_tqdm() > 10 * time_bench():
|
|
||||||
raise AssertionError('tqdm(%g): %f, range(%g): %f' %
|
|
||||||
(total, time_tqdm(), total, time_bench()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
@retry_on_except()
|
|
||||||
def test_iter_overhead_hard():
|
|
||||||
"""Test overhead of iteration based tqdm (hard)"""
|
|
||||||
|
|
||||||
total = int(1e5)
|
|
||||||
|
|
||||||
with closing(MockIO()) as our_file:
|
|
||||||
a = 0
|
|
||||||
with trange(total, file=our_file, leave=True, miniters=1,
|
|
||||||
mininterval=0, maxinterval=0) as t:
|
|
||||||
with relative_timer() as time_tqdm:
|
|
||||||
for i in t:
|
|
||||||
a += i
|
|
||||||
assert (a == (total * total - total) / 2.0)
|
|
||||||
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_bench:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
our_file.write(("%i" % a) * 40)
|
|
||||||
|
|
||||||
# Compute relative overhead of tqdm against native range()
|
|
||||||
try:
|
|
||||||
assert (time_tqdm() < 60 * time_bench())
|
|
||||||
except AssertionError:
|
|
||||||
raise AssertionError('trange(%g): %f, range(%g): %f' %
|
|
||||||
(total, time_tqdm(), total, time_bench()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
@retry_on_except()
|
|
||||||
def test_manual_overhead_hard():
|
|
||||||
"""Test overhead of manual tqdm (hard)"""
|
|
||||||
|
|
||||||
total = int(1e5)
|
|
||||||
|
|
||||||
with closing(MockIO()) as our_file:
|
|
||||||
t = tqdm(total=total * 10, file=our_file, leave=True, miniters=1,
|
|
||||||
mininterval=0, maxinterval=0)
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_tqdm:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
t.update(10)
|
|
||||||
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_bench:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
our_file.write(("%i" % a) * 40)
|
|
||||||
|
|
||||||
# Compute relative overhead of tqdm against native range()
|
|
||||||
try:
|
|
||||||
assert (time_tqdm() < 100 * time_bench())
|
|
||||||
except AssertionError:
|
|
||||||
raise AssertionError('tqdm(%g): %f, range(%g): %f' %
|
|
||||||
(total, time_tqdm(), total, time_bench()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
@retry_on_except()
|
|
||||||
def test_iter_overhead_simplebar_hard():
|
|
||||||
"""Test overhead of iteration based tqdm vs simple progress bar (hard)"""
|
|
||||||
|
|
||||||
total = int(1e4)
|
|
||||||
|
|
||||||
with closing(MockIO()) as our_file:
|
|
||||||
a = 0
|
|
||||||
with trange(total, file=our_file, leave=True, miniters=1,
|
|
||||||
mininterval=0, maxinterval=0) as t:
|
|
||||||
with relative_timer() as time_tqdm:
|
|
||||||
for i in t:
|
|
||||||
a += i
|
|
||||||
assert (a == (total * total - total) / 2.0)
|
|
||||||
|
|
||||||
a = 0
|
|
||||||
s = simple_progress(_range(total), file=our_file, leave=True,
|
|
||||||
miniters=1, mininterval=0)
|
|
||||||
with relative_timer() as time_bench:
|
|
||||||
for i in s:
|
|
||||||
a += i
|
|
||||||
|
|
||||||
# Compute relative overhead of tqdm against native range()
|
|
||||||
try:
|
|
||||||
assert (time_tqdm() < 2.5 * time_bench())
|
|
||||||
except AssertionError:
|
|
||||||
raise AssertionError('trange(%g): %f, simple_progress(%g): %f' %
|
|
||||||
(total, time_tqdm(), total, time_bench()))
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(pretest, posttest)
|
|
||||||
@retry_on_except()
|
|
||||||
def test_manual_overhead_simplebar_hard():
|
|
||||||
"""Test overhead of manual tqdm vs simple progress bar (hard)"""
|
|
||||||
|
|
||||||
total = int(1e4)
|
|
||||||
|
|
||||||
with closing(MockIO()) as our_file:
|
|
||||||
t = tqdm(total=total * 10, file=our_file, leave=True, miniters=1,
|
|
||||||
mininterval=0, maxinterval=0)
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_tqdm:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
t.update(10)
|
|
||||||
|
|
||||||
simplebar_update = simple_progress(
|
|
||||||
total=total, file=our_file, leave=True, miniters=1, mininterval=0)
|
|
||||||
a = 0
|
|
||||||
with relative_timer() as time_bench:
|
|
||||||
for i in _range(total):
|
|
||||||
a += i
|
|
||||||
simplebar_update(10)
|
|
||||||
|
|
||||||
# Compute relative overhead of tqdm against native range()
|
|
||||||
try:
|
|
||||||
assert (time_tqdm() < 2.5 * time_bench())
|
|
||||||
except AssertionError:
|
|
||||||
raise AssertionError('tqdm(%g): %f, simple_progress(%g): %f' %
|
|
||||||
(total, time_tqdm(), total, time_bench()))
|
|
||||||